From fe9ac76d2abd5578c03bb5552a8365e126c8db22 Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:05:58 +0900 Subject: [PATCH 01/15] feat: Add GPT-5.1 model and implement migration from v12 to v13 - Added GPT-5.1 to default chat models with pricing ($1.25 input, $10 output per million tokens) - Updated recommended models to include GPT-5.1 instead of GPT-4.1 - Introduced migration logic to transition settings from version 12 to 13 - Migration adds GPT-5.1 to user settings while preserving existing GPT-5 as custom model - Removed GPT-5 from default models (users keep it as custom model) - Added comprehensive tests to verify migration functionality --- src/constants.ts | 7 +- .../schema/migrations/12_to_13.test.ts | 121 ++++++++++ src/settings/schema/migrations/12_to_13.ts | 220 ++++++++++++++++++ src/settings/schema/migrations/index.ts | 8 +- 4 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 src/settings/schema/migrations/12_to_13.test.ts create mode 100644 src/settings/schema/migrations/12_to_13.ts diff --git a/src/constants.ts b/src/constants.ts index e46d4802..8fadb45e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,7 +12,7 @@ export const DEFAULT_CHAT_MODEL_ID = 'claude-sonnet-4.5' export const DEFAULT_APPLY_MODEL_ID = 'gpt-4.1-mini' // Recommended model ids -export const RECOMMENDED_MODELS_FOR_CHAT = ['claude-sonnet-4.5', 'gpt-4.1'] +export const RECOMMENDED_MODELS_FOR_CHAT = ['claude-sonnet-4.5', 'gpt-5.1'] export const RECOMMENDED_MODELS_FOR_APPLY = ['gpt-4.1-mini'] export const RECOMMENDED_MODELS_FOR_EMBEDDING = [ 'openai/text-embedding-3-small', @@ -245,8 +245,8 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ { providerType: 'openai', providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-5', - model: 'gpt-5', + id: 'gpt-5.1', + model: 'gpt-5.1', }, { providerType: 'openai', @@ -462,6 +462,7 @@ type ModelPricing = { } export const OPENAI_PRICES: Record = { + 'gpt-5.1': { input: 1.25, output: 10 }, 'gpt-5': { input: 1.25, output: 10 }, 'gpt-5-mini': { input: 0.25, output: 2 }, 'gpt-5-nano': { input: 0.05, output: 0.4 }, diff --git a/src/settings/schema/migrations/12_to_13.test.ts b/src/settings/schema/migrations/12_to_13.test.ts new file mode 100644 index 00000000..7002f785 --- /dev/null +++ b/src/settings/schema/migrations/12_to_13.test.ts @@ -0,0 +1,121 @@ +import { DEFAULT_CHAT_MODELS_V13, migrateFrom12To13 } from './12_to_13' + +describe('Migration from v12 to v13', () => { + it('should increment version to 13', () => { + const oldSettings = { + version: 12, + } + const result = migrateFrom12To13(oldSettings) + expect(result.version).toBe(13) + }) + + it('should merge existing chat models with new default models', () => { + const oldSettings = { + version: 12, + chatModels: [ + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + enable: false, + }, + { + id: 'custom-model', + providerType: 'custom', + providerId: 'custom', + model: 'custom-model', + }, + ], + } + const result = migrateFrom12To13(oldSettings) + + expect(result.chatModels).toEqual([ + ...DEFAULT_CHAT_MODELS_V13.map((model) => + model.id === 'gpt-4o' + ? { + ...model, + enable: false, + } + : model, + ), + { + id: 'custom-model', + providerType: 'custom', + providerId: 'custom', + model: 'custom-model', + }, + ]) + }) + + it('should add new GPT-5.1 model', () => { + const oldSettings = { + version: 12, + chatModels: [ + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + ], + } + const result = migrateFrom12To13(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') + + expect(gpt51).toBeDefined() + expect(gpt51).toEqual({ + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + }) + }) + + it('should preserve gpt-5 as custom model when migrating', () => { + const oldSettings = { + version: 12, + chatModels: [ + { + id: 'gpt-5', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5', + enable: true, + }, + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + ], + } + const result = migrateFrom12To13(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt5 = chatModels.find((m) => m.id === 'gpt-5') + const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') + + // gpt-5 should be preserved as custom model + expect(gpt5).toBeDefined() + expect(gpt5).toEqual({ + id: 'gpt-5', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5', + enable: true, + }) + + // gpt-5.1 should be added as new default + expect(gpt51).toBeDefined() + expect(gpt51).toEqual({ + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + }) + }) +}) diff --git a/src/settings/schema/migrations/12_to_13.ts b/src/settings/schema/migrations/12_to_13.ts new file mode 100644 index 00000000..26f64652 --- /dev/null +++ b/src/settings/schema/migrations/12_to_13.ts @@ -0,0 +1,220 @@ +import { SettingMigration } from '../setting.types' + +import { getMigratedChatModels } from './migrationUtils' + +/** + * Migration from version 12 to version 13 + * - Add following models: + * - gpt-5.1 + * - Remove following models from defaults: + * - gpt-5 + */ +export const migrateFrom12To13: SettingMigration['migrate'] = (data) => { + const newData = { ...data } + newData.version = 13 + + newData.chatModels = getMigratedChatModels(newData, DEFAULT_CHAT_MODELS_V13) + + return newData +} + +type DefaultChatModelsV13 = { + id: string + providerType: string + providerId: string + model: string + reasoning?: { + enabled: boolean + reasoning_effort?: string + } + thinking?: { + enabled: boolean + budget_tokens: number + } + web_search_options?: { + search_context_size?: string + } + enable?: boolean +}[] + +export const DEFAULT_CHAT_MODELS_V13: DefaultChatModelsV13 = [ + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-opus-4.1', + model: 'claude-opus-4-1', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-sonnet-4.5', + model: 'claude-sonnet-4-5', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-haiku-4.5', + model: 'claude-haiku-4-5', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5.1', + model: 'gpt-5.1', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-mini', + model: 'gpt-5-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-nano', + model: 'gpt-5-nano', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1', + model: 'gpt-4.1', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1-mini', + model: 'gpt-4.1-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1-nano', + model: 'gpt-4.1-nano', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o', + model: 'gpt-4o', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o-mini', + model: 'gpt-4o-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o4-mini', + model: 'o4-mini', + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o3', + model: 'o3', + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-pro', + model: 'gemini-2.5-pro', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash', + model: 'gemini-2.5-flash', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash-lite', + model: 'gemini-2.5-flash-lite', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash', + model: 'gemini-2.0-flash', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash-lite', + model: 'gemini-2.0-flash-lite', + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-chat', + model: 'deepseek-chat', + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-reasoner', + model: 'deepseek-reasoner', + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-pro', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-deep-research', + model: 'sonar-deep-research', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning-pro', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'morph', + providerId: 'morph', + id: 'morph-v0', + model: 'morph-v0', + }, +] diff --git a/src/settings/schema/migrations/index.ts b/src/settings/schema/migrations/index.ts index f3842551..a61e1d07 100644 --- a/src/settings/schema/migrations/index.ts +++ b/src/settings/schema/migrations/index.ts @@ -3,6 +3,7 @@ import { SettingMigration } from '../setting.types' import { migrateFrom0To1 } from './0_to_1' import { migrateFrom10To11 } from './10_to_11' import { migrateFrom11To12 } from './11_to_12' +import { migrateFrom12To13 } from './12_to_13' import { migrateFrom1To2 } from './1_to_2' import { migrateFrom2To3 } from './2_to_3' import { migrateFrom3To4 } from './3_to_4' @@ -13,7 +14,7 @@ import { migrateFrom7To8 } from './7_to_8' import { migrateFrom8To9 } from './8_to_9' import { migrateFrom9To10 } from './9_to_10' -export const SETTINGS_SCHEMA_VERSION = 12 +export const SETTINGS_SCHEMA_VERSION = 13 export const SETTING_MIGRATIONS: SettingMigration[] = [ { @@ -76,4 +77,9 @@ export const SETTING_MIGRATIONS: SettingMigration[] = [ toVersion: 12, migrate: migrateFrom11To12, }, + { + fromVersion: 12, + toVersion: 13, + migrate: migrateFrom12To13, + }, ] From bd2ec3b6f22a4db8917dbb5ab312d99cc30c864c Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:23:20 +0900 Subject: [PATCH 02/15] feat: Remove GPT-4.1 models from defaults - Removed gpt-4.1, gpt-4.1-mini, and gpt-4.1-nano from default chat models - Updated migration v12 to v13 to handle removal of GPT-4.1 models - Added test to verify GPT-4.1 models are preserved as custom models during migration - Users with existing GPT-4.1 models will keep them as custom models --- src/constants.ts | 18 ----- .../schema/migrations/12_to_13.test.ts | 75 +++++++++++++++++++ src/settings/schema/migrations/12_to_13.ts | 21 +----- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 8fadb45e..a38dc8aa 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -260,24 +260,6 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ id: 'gpt-5-nano', model: 'gpt-5-nano', }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4.1', - model: 'gpt-4.1', - }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4.1-mini', - model: 'gpt-4.1-mini', - }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4.1-nano', - model: 'gpt-4.1-nano', - }, { providerType: 'openai', providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, diff --git a/src/settings/schema/migrations/12_to_13.test.ts b/src/settings/schema/migrations/12_to_13.test.ts index 7002f785..fe6660ed 100644 --- a/src/settings/schema/migrations/12_to_13.test.ts +++ b/src/settings/schema/migrations/12_to_13.test.ts @@ -118,4 +118,79 @@ describe('Migration from v12 to v13', () => { model: 'gpt-5.1', }) }) + + it('should preserve gpt-4.1 models as custom models when migrating', () => { + const oldSettings = { + version: 12, + chatModels: [ + { + id: 'gpt-4.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1', + enable: true, + }, + { + id: 'gpt-4.1-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-mini', + }, + { + id: 'gpt-4.1-nano', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-nano', + }, + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + ], + } + const result = migrateFrom12To13(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt41 = chatModels.find((m) => m.id === 'gpt-4.1') + const gpt41Mini = chatModels.find((m) => m.id === 'gpt-4.1-mini') + const gpt41Nano = chatModels.find((m) => m.id === 'gpt-4.1-nano') + const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') + + // gpt-4.1 models should be preserved as custom models + expect(gpt41).toBeDefined() + expect(gpt41).toEqual({ + id: 'gpt-4.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1', + enable: true, + }) + + expect(gpt41Mini).toBeDefined() + expect(gpt41Mini).toEqual({ + id: 'gpt-4.1-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-mini', + }) + + expect(gpt41Nano).toBeDefined() + expect(gpt41Nano).toEqual({ + id: 'gpt-4.1-nano', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-nano', + }) + + // gpt-5.1 should be added as new default + expect(gpt51).toBeDefined() + expect(gpt51).toEqual({ + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + }) + }) }) diff --git a/src/settings/schema/migrations/12_to_13.ts b/src/settings/schema/migrations/12_to_13.ts index 26f64652..78de0312 100644 --- a/src/settings/schema/migrations/12_to_13.ts +++ b/src/settings/schema/migrations/12_to_13.ts @@ -8,6 +8,9 @@ import { getMigratedChatModels } from './migrationUtils' * - gpt-5.1 * - Remove following models from defaults: * - gpt-5 + * - gpt-4.1 + * - gpt-4.1-mini + * - gpt-4.1-nano */ export const migrateFrom12To13: SettingMigration['migrate'] = (data) => { const newData = { ...data } @@ -74,24 +77,6 @@ export const DEFAULT_CHAT_MODELS_V13: DefaultChatModelsV13 = [ id: 'gpt-5-nano', model: 'gpt-5-nano', }, - { - providerType: 'openai', - providerId: 'openai', - id: 'gpt-4.1', - model: 'gpt-4.1', - }, - { - providerType: 'openai', - providerId: 'openai', - id: 'gpt-4.1-mini', - model: 'gpt-4.1-mini', - }, - { - providerType: 'openai', - providerId: 'openai', - id: 'gpt-4.1-nano', - model: 'gpt-4.1-nano', - }, { providerType: 'openai', providerId: 'openai', From cd0e09921dee4b04c27850e46fbff92c0db7092a Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:59:23 +0900 Subject: [PATCH 03/15] feat: Add support for predicted outputs in specific chat models - Introduced a new utility function to check if a model supports predicted outputs. - Updated the applyChangesToFile function to conditionally include prediction logic for supported models: gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini, and gpt-4.1-nano. --- src/utils/chat/apply.ts | 44 +++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/utils/chat/apply.ts b/src/utils/chat/apply.ts index 7dcf5d55..969f8b11 100644 --- a/src/utils/chat/apply.ts +++ b/src/utils/chat/apply.ts @@ -15,6 +15,20 @@ import { LLMProvider } from '../../types/provider.types' const MAX_CHAT_HISTORY_MESSAGES = 10 +const PREDICTED_OUTPUTS_SUPPORTED_MODELS = [ + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', +] + +const supportsPredictedOutputs = (modelId: string): boolean => { + return PREDICTED_OUTPUTS_SUPPORTED_MODELS.some((supportedModel) => + modelId.startsWith(supportedModel), + ) +} + const systemPrompt = `You are an intelligent assistant helping a user apply changes to a markdown file. You will receive: @@ -142,21 +156,21 @@ export const applyChangesToFile = async ({ model: model.model, messages: requestMessages, stream: false, - - // prediction is only available for OpenAI - prediction: { - type: 'content', - content: [ - { - type: 'text', - text: currentFileContent, - }, - { - type: 'text', - text: blockToApply, - }, - ], - }, + ...(supportsPredictedOutputs(model.model) && { + prediction: { + type: 'content', + content: [ + { + type: 'text', + text: currentFileContent, + }, + { + type: 'text', + text: blockToApply, + }, + ], + }, + }), }) const responseContent = response.choices[0].message.content From b2f2eb696d66fb0cca877d05d9263a5b7bf06ae2 Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:05:10 +0900 Subject: [PATCH 04/15] chore: Update default apply model ID to gpt-4.1-mini with explanatory comment - Changed the default apply model ID to 'gpt-4.1-mini' and added a comment explaining its preference over gpt-5-mini due to performance considerations regarding predicted outputs. --- src/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/constants.ts b/src/constants.ts index a38dc8aa..4b0a2515 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,8 @@ export const PGLITE_DB_PATH = '.smtcmp_vector_db.tar.gz' // Default model ids export const DEFAULT_CHAT_MODEL_ID = 'claude-sonnet-4.5' +// gpt-4.1-mini is preferred over gpt-5-mini because gpt-5 models do not support +// predicted outputs, making them significantly slower for apply tasks. export const DEFAULT_APPLY_MODEL_ID = 'gpt-4.1-mini' // Recommended model ids From 1d74d35830188d8c82bc4462e3ef94d8a219843e Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:15:10 +0900 Subject: [PATCH 05/15] feat: Update recommended models and schema version - Changed the recommended chat model from 'gpt-5.1' to 'gpt-5.2'. - Updated the pricing for 'gpt-5.2' in the OPENAI_PRICES record. - Incremented the SETTINGS_SCHEMA_VERSION to 14 and added migration logic for version 13 to 14. --- src/constants.ts | 7 +- .../schema/migrations/13_to_14.test.ts | 121 +++++++++++ src/settings/schema/migrations/13_to_14.ts | 200 ++++++++++++++++++ src/settings/schema/migrations/index.ts | 8 +- 4 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 src/settings/schema/migrations/13_to_14.test.ts create mode 100644 src/settings/schema/migrations/13_to_14.ts diff --git a/src/constants.ts b/src/constants.ts index 4b0a2515..c665dd5a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,7 +14,7 @@ export const DEFAULT_CHAT_MODEL_ID = 'claude-sonnet-4.5' export const DEFAULT_APPLY_MODEL_ID = 'gpt-4.1-mini' // Recommended model ids -export const RECOMMENDED_MODELS_FOR_CHAT = ['claude-sonnet-4.5', 'gpt-5.1'] +export const RECOMMENDED_MODELS_FOR_CHAT = ['claude-sonnet-4.5', 'gpt-5.2'] export const RECOMMENDED_MODELS_FOR_APPLY = ['gpt-4.1-mini'] export const RECOMMENDED_MODELS_FOR_EMBEDDING = [ 'openai/text-embedding-3-small', @@ -247,8 +247,8 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ { providerType: 'openai', providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-5.1', - model: 'gpt-5.1', + id: 'gpt-5.2', + model: 'gpt-5.2', }, { providerType: 'openai', @@ -446,6 +446,7 @@ type ModelPricing = { } export const OPENAI_PRICES: Record = { + 'gpt-5.2': { input: 1.25, output: 10 }, 'gpt-5.1': { input: 1.25, output: 10 }, 'gpt-5': { input: 1.25, output: 10 }, 'gpt-5-mini': { input: 0.25, output: 2 }, diff --git a/src/settings/schema/migrations/13_to_14.test.ts b/src/settings/schema/migrations/13_to_14.test.ts new file mode 100644 index 00000000..779430f4 --- /dev/null +++ b/src/settings/schema/migrations/13_to_14.test.ts @@ -0,0 +1,121 @@ +import { DEFAULT_CHAT_MODELS_V14, migrateFrom13To14 } from './13_to_14' + +describe('Migration from v13 to v14', () => { + it('should increment version to 14', () => { + const oldSettings = { + version: 13, + } + const result = migrateFrom13To14(oldSettings) + expect(result.version).toBe(14) + }) + + it('should merge existing chat models with new default models', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + enable: false, + }, + { + id: 'custom-model', + providerType: 'custom', + providerId: 'custom', + model: 'custom-model', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + expect(result.chatModels).toEqual([ + ...DEFAULT_CHAT_MODELS_V14.map((model) => + model.id === 'gpt-4o' + ? { + ...model, + enable: false, + } + : model, + ), + { + id: 'custom-model', + providerType: 'custom', + providerId: 'custom', + model: 'custom-model', + }, + ]) + }) + + it('should add new GPT-5.2 model', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt52 = chatModels.find((m) => m.id === 'gpt-5.2') + + expect(gpt52).toBeDefined() + expect(gpt52).toEqual({ + id: 'gpt-5.2', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.2', + }) + }) + + it('should preserve gpt-5.1 as custom model when migrating', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + enable: true, + }, + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') + const gpt52 = chatModels.find((m) => m.id === 'gpt-5.2') + + // gpt-5.1 should be preserved as custom model + expect(gpt51).toBeDefined() + expect(gpt51).toEqual({ + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + enable: true, + }) + + // gpt-5.2 should be added as new default + expect(gpt52).toBeDefined() + expect(gpt52).toEqual({ + id: 'gpt-5.2', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.2', + }) + }) +}) diff --git a/src/settings/schema/migrations/13_to_14.ts b/src/settings/schema/migrations/13_to_14.ts new file mode 100644 index 00000000..d43c6c58 --- /dev/null +++ b/src/settings/schema/migrations/13_to_14.ts @@ -0,0 +1,200 @@ +import { SettingMigration } from '../setting.types' + +import { getMigratedChatModels } from './migrationUtils' + +/** + * Migration from version 13 to version 14 + * - Add following models: + * - gpt-5.2 + */ +export const migrateFrom13To14: SettingMigration['migrate'] = (data) => { + const newData = { ...data } + newData.version = 14 + + newData.chatModels = getMigratedChatModels(newData, DEFAULT_CHAT_MODELS_V14) + + return newData +} + +type DefaultChatModelsV14 = { + id: string + providerType: string + providerId: string + model: string + reasoning?: { + enabled: boolean + reasoning_effort?: string + } + thinking?: { + enabled: boolean + budget_tokens: number + } + web_search_options?: { + search_context_size?: string + } + enable?: boolean +}[] + +export const DEFAULT_CHAT_MODELS_V14: DefaultChatModelsV14 = [ + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-opus-4.1', + model: 'claude-opus-4-1', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-sonnet-4.5', + model: 'claude-sonnet-4-5', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-haiku-4.5', + model: 'claude-haiku-4-5', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5.2', + model: 'gpt-5.2', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-mini', + model: 'gpt-5-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-nano', + model: 'gpt-5-nano', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o', + model: 'gpt-4o', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o-mini', + model: 'gpt-4o-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o4-mini', + model: 'o4-mini', + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o3', + model: 'o3', + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-pro', + model: 'gemini-2.5-pro', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash', + model: 'gemini-2.5-flash', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash-lite', + model: 'gemini-2.5-flash-lite', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash', + model: 'gemini-2.0-flash', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash-lite', + model: 'gemini-2.0-flash-lite', + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-chat', + model: 'deepseek-chat', + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-reasoner', + model: 'deepseek-reasoner', + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-pro', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-deep-research', + model: 'sonar-deep-research', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning-pro', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'morph', + providerId: 'morph', + id: 'morph-v0', + model: 'morph-v0', + }, +] diff --git a/src/settings/schema/migrations/index.ts b/src/settings/schema/migrations/index.ts index a61e1d07..03d1674b 100644 --- a/src/settings/schema/migrations/index.ts +++ b/src/settings/schema/migrations/index.ts @@ -4,6 +4,7 @@ import { migrateFrom0To1 } from './0_to_1' import { migrateFrom10To11 } from './10_to_11' import { migrateFrom11To12 } from './11_to_12' import { migrateFrom12To13 } from './12_to_13' +import { migrateFrom13To14 } from './13_to_14' import { migrateFrom1To2 } from './1_to_2' import { migrateFrom2To3 } from './2_to_3' import { migrateFrom3To4 } from './3_to_4' @@ -14,7 +15,7 @@ import { migrateFrom7To8 } from './7_to_8' import { migrateFrom8To9 } from './8_to_9' import { migrateFrom9To10 } from './9_to_10' -export const SETTINGS_SCHEMA_VERSION = 13 +export const SETTINGS_SCHEMA_VERSION = 14 export const SETTING_MIGRATIONS: SettingMigration[] = [ { @@ -82,4 +83,9 @@ export const SETTING_MIGRATIONS: SettingMigration[] = [ toVersion: 13, migrate: migrateFrom12To13, }, + { + fromVersion: 13, + toVersion: 14, + migrate: migrateFrom13To14, + }, ] From 7ade3c7d41aa9b7247691f778674d9ea7e42dfc9 Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:26:20 +0900 Subject: [PATCH 06/15] feat: Update chat models and migration logic for version 14 - Removed outdated models 'gpt-5-nano', 'gpt-4o', and 'gpt-4o-mini' from default chat models. - Added 'gpt-4.1-mini' to the default chat models and migration logic. - Updated the test cases to reflect changes in model IDs and ensure proper migration from version 13 to 14. - Adjusted pricing for 'gpt-5.2' in the OPENAI_PRICES record. --- src/constants.ts | 28 ++----------- .../schema/migrations/13_to_14.test.ts | 40 +++++++++++++++---- src/settings/schema/migrations/13_to_14.ts | 27 ++----------- 3 files changed, 39 insertions(+), 56 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index c665dd5a..2d95a8c4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -259,20 +259,8 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ { providerType: 'openai', providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-5-nano', - model: 'gpt-5-nano', - }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4o', - model: 'gpt-4o', - }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4o-mini', - model: 'gpt-4o-mini', + id: 'gpt-4.1-mini', + model: 'gpt-4.1-mini', }, { providerType: 'openai', @@ -284,16 +272,6 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ reasoning_effort: 'medium', }, }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'o3', - model: 'o3', - reasoning: { - enabled: true, - reasoning_effort: 'medium', - }, - }, { providerType: 'gemini', providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, @@ -446,7 +424,7 @@ type ModelPricing = { } export const OPENAI_PRICES: Record = { - 'gpt-5.2': { input: 1.25, output: 10 }, + 'gpt-5.2': { input: 1.75, output: 14 }, 'gpt-5.1': { input: 1.25, output: 10 }, 'gpt-5': { input: 1.25, output: 10 }, 'gpt-5-mini': { input: 0.25, output: 2 }, diff --git a/src/settings/schema/migrations/13_to_14.test.ts b/src/settings/schema/migrations/13_to_14.test.ts index 779430f4..ca77bdf3 100644 --- a/src/settings/schema/migrations/13_to_14.test.ts +++ b/src/settings/schema/migrations/13_to_14.test.ts @@ -14,10 +14,10 @@ describe('Migration from v13 to v14', () => { version: 13, chatModels: [ { - id: 'gpt-4o', + id: 'gpt-5-mini', providerType: 'openai', providerId: 'openai', - model: 'gpt-4o', + model: 'gpt-5-mini', enable: false, }, { @@ -32,7 +32,7 @@ describe('Migration from v13 to v14', () => { expect(result.chatModels).toEqual([ ...DEFAULT_CHAT_MODELS_V14.map((model) => - model.id === 'gpt-4o' + model.id === 'gpt-5-mini' ? { ...model, enable: false, @@ -53,10 +53,10 @@ describe('Migration from v13 to v14', () => { version: 13, chatModels: [ { - id: 'gpt-4o', + id: 'gpt-5-mini', providerType: 'openai', providerId: 'openai', - model: 'gpt-4o', + model: 'gpt-5-mini', }, ], } @@ -74,6 +74,32 @@ describe('Migration from v13 to v14', () => { }) }) + it('should add new gpt-4.1-mini model', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'gpt-5-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5-mini', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt41mini = chatModels.find((m) => m.id === 'gpt-4.1-mini') + + expect(gpt41mini).toBeDefined() + expect(gpt41mini).toEqual({ + id: 'gpt-4.1-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-mini', + }) + }) + it('should preserve gpt-5.1 as custom model when migrating', () => { const oldSettings = { version: 13, @@ -86,10 +112,10 @@ describe('Migration from v13 to v14', () => { enable: true, }, { - id: 'gpt-4o', + id: 'gpt-5-mini', providerType: 'openai', providerId: 'openai', - model: 'gpt-4o', + model: 'gpt-5-mini', }, ], } diff --git a/src/settings/schema/migrations/13_to_14.ts b/src/settings/schema/migrations/13_to_14.ts index d43c6c58..4ef819aa 100644 --- a/src/settings/schema/migrations/13_to_14.ts +++ b/src/settings/schema/migrations/13_to_14.ts @@ -6,6 +6,7 @@ import { getMigratedChatModels } from './migrationUtils' * Migration from version 13 to version 14 * - Add following models: * - gpt-5.2 + * - gpt-4.1-mini */ export const migrateFrom13To14: SettingMigration['migrate'] = (data) => { const newData = { ...data } @@ -69,20 +70,8 @@ export const DEFAULT_CHAT_MODELS_V14: DefaultChatModelsV14 = [ { providerType: 'openai', providerId: 'openai', - id: 'gpt-5-nano', - model: 'gpt-5-nano', - }, - { - providerType: 'openai', - providerId: 'openai', - id: 'gpt-4o', - model: 'gpt-4o', - }, - { - providerType: 'openai', - providerId: 'openai', - id: 'gpt-4o-mini', - model: 'gpt-4o-mini', + id: 'gpt-4.1-mini', + model: 'gpt-4.1-mini', }, { providerType: 'openai', @@ -94,16 +83,6 @@ export const DEFAULT_CHAT_MODELS_V14: DefaultChatModelsV14 = [ reasoning_effort: 'medium', }, }, - { - providerType: 'openai', - providerId: 'openai', - id: 'o3', - model: 'o3', - reasoning: { - enabled: true, - reasoning_effort: 'medium', - }, - }, { providerType: 'gemini', providerId: 'gemini', From c855cf2de8c45ab40210ef0a6c8b6a5876d5532b Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:34:35 +0900 Subject: [PATCH 07/15] feat: Update default chat model to claude-opus-4.5 and enhance migration tests - Updated the default chat model from 'claude-opus-4.1' to 'claude-opus-4.5' in constants and migration logic. - Added tests to ensure 'claude-opus-4.5' is correctly added as a new default model during migration from version 13 to 14. - Preserved 'claude-opus-4.1' as a custom model in migration tests. --- src/constants.ts | 5 +- .../schema/migrations/13_to_14.test.ts | 66 +++++++++++++++++++ src/settings/schema/migrations/13_to_14.ts | 12 +++- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2d95a8c4..1dcee4b6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -229,8 +229,8 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ { providerType: 'anthropic', providerId: PROVIDER_TYPES_INFO.anthropic.defaultProviderId, - id: 'claude-opus-4.1', - model: 'claude-opus-4-1', + id: 'claude-opus-4.5', + model: 'claude-opus-4-5', }, { providerType: 'anthropic', @@ -442,6 +442,7 @@ export const OPENAI_PRICES: Record = { } export const ANTHROPIC_PRICES: Record = { + 'claude-opus-4-5': { input: 5, output: 25 }, 'claude-opus-4-1': { input: 15, output: 75 }, 'claude-opus-4-0': { input: 15, output: 75 }, 'claude-sonnet-4-5': { input: 3, output: 15 }, diff --git a/src/settings/schema/migrations/13_to_14.test.ts b/src/settings/schema/migrations/13_to_14.test.ts index ca77bdf3..9ebe470b 100644 --- a/src/settings/schema/migrations/13_to_14.test.ts +++ b/src/settings/schema/migrations/13_to_14.test.ts @@ -144,4 +144,70 @@ describe('Migration from v13 to v14', () => { model: 'gpt-5.2', }) }) + + it('should add claude-opus-4.5 as new default model', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'claude-sonnet-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-sonnet-4-5', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const opus45 = chatModels.find((m) => m.id === 'claude-opus-4.5') + + // claude-opus-4.5 should be added as new default + expect(opus45).toBeDefined() + expect(opus45).toEqual({ + id: 'claude-opus-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-opus-4-5', + }) + }) + + it('should preserve claude-opus-4.1 as custom model when migrating', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'claude-opus-4.1', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-opus-4-1', + enable: true, + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const opus41 = chatModels.find((m) => m.id === 'claude-opus-4.1') + const opus45 = chatModels.find((m) => m.id === 'claude-opus-4.5') + + // claude-opus-4.1 should be preserved as custom model + expect(opus41).toBeDefined() + expect(opus41).toEqual({ + id: 'claude-opus-4.1', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-opus-4-1', + enable: true, + }) + + // claude-opus-4.5 should be added as new default + expect(opus45).toBeDefined() + expect(opus45).toEqual({ + id: 'claude-opus-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-opus-4-5', + }) + }) }) diff --git a/src/settings/schema/migrations/13_to_14.ts b/src/settings/schema/migrations/13_to_14.ts index 4ef819aa..022f3e4e 100644 --- a/src/settings/schema/migrations/13_to_14.ts +++ b/src/settings/schema/migrations/13_to_14.ts @@ -7,6 +7,14 @@ import { getMigratedChatModels } from './migrationUtils' * - Add following models: * - gpt-5.2 * - gpt-4.1-mini + * - Update following models: + * - claude-opus-4.1 -> claude-opus-4.5 + * - Remove following models from defaults: + * - gpt-5.1 + * - gpt-5-nano + * - gpt-4o + * - gpt-4o-mini + * - o3 */ export const migrateFrom13To14: SettingMigration['migrate'] = (data) => { const newData = { ...data } @@ -40,8 +48,8 @@ export const DEFAULT_CHAT_MODELS_V14: DefaultChatModelsV14 = [ { providerType: 'anthropic', providerId: 'anthropic', - id: 'claude-opus-4.1', - model: 'claude-opus-4-1', + id: 'claude-opus-4.5', + model: 'claude-opus-4-5', }, { providerType: 'anthropic', From 11e55128d13f763f238d87008e527c74738c88c6 Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:41:35 +0900 Subject: [PATCH 08/15] feat: Introduce new default models for migration from v13 to v14 - Added 'gemini-3-pro-preview' and 'gemini-3-flash-preview' as new default chat models in the migration logic. - Updated the constants to reflect the new model IDs and removed outdated models. - Enhanced migration tests to ensure the new models are correctly added and existing models are preserved during migration. --- src/constants.ts | 26 +------ .../schema/migrations/13_to_14.test.ts | 78 +++++++++++++++++++ src/settings/schema/migrations/13_to_14.ts | 33 +++----- 3 files changed, 93 insertions(+), 44 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 1dcee4b6..db0de6c3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -275,32 +275,14 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ { providerType: 'gemini', providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.5-pro', - model: 'gemini-2.5-pro', + id: 'gemini-3-pro-preview', + model: 'gemini-3-pro-preview', }, { providerType: 'gemini', providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.5-flash', - model: 'gemini-2.5-flash', - }, - { - providerType: 'gemini', - providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.5-flash-lite', - model: 'gemini-2.5-flash-lite', - }, - { - providerType: 'gemini', - providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.0-flash', - model: 'gemini-2.0-flash', - }, - { - providerType: 'gemini', - providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.0-flash-lite', - model: 'gemini-2.0-flash-lite', + id: 'gemini-3-flash-preview', + model: 'gemini-3-flash-preview', }, { providerType: 'deepseek', diff --git a/src/settings/schema/migrations/13_to_14.test.ts b/src/settings/schema/migrations/13_to_14.test.ts index 9ebe470b..b387c639 100644 --- a/src/settings/schema/migrations/13_to_14.test.ts +++ b/src/settings/schema/migrations/13_to_14.test.ts @@ -210,4 +210,82 @@ describe('Migration from v13 to v14', () => { model: 'claude-opus-4-5', }) }) + + it('should add gemini-3-pro-preview and gemini-3-flash-preview as new default models', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'gpt-5-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5-mini', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gemini3Pro = chatModels.find((m) => m.id === 'gemini-3-pro-preview') + const gemini3Flash = chatModels.find( + (m) => m.id === 'gemini-3-flash-preview', + ) + + // gemini-3-pro-preview should be added as new default + expect(gemini3Pro).toBeDefined() + expect(gemini3Pro).toEqual({ + id: 'gemini-3-pro-preview', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-3-pro-preview', + }) + + // gemini-3-flash-preview should be added as new default + expect(gemini3Flash).toBeDefined() + expect(gemini3Flash).toEqual({ + id: 'gemini-3-flash-preview', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-3-flash-preview', + }) + }) + + it('should preserve gemini-2.5-pro as custom model when migrating', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'gemini-2.5-pro', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-2.5-pro', + enable: true, + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gemini25Pro = chatModels.find((m) => m.id === 'gemini-2.5-pro') + const gemini3Pro = chatModels.find((m) => m.id === 'gemini-3-pro-preview') + + // gemini-2.5-pro should be preserved as custom model + expect(gemini25Pro).toBeDefined() + expect(gemini25Pro).toEqual({ + id: 'gemini-2.5-pro', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-2.5-pro', + enable: true, + }) + + // gemini-3-pro-preview should be added as new default + expect(gemini3Pro).toBeDefined() + expect(gemini3Pro).toEqual({ + id: 'gemini-3-pro-preview', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-3-pro-preview', + }) + }) }) diff --git a/src/settings/schema/migrations/13_to_14.ts b/src/settings/schema/migrations/13_to_14.ts index 022f3e4e..e79e22ed 100644 --- a/src/settings/schema/migrations/13_to_14.ts +++ b/src/settings/schema/migrations/13_to_14.ts @@ -7,6 +7,8 @@ import { getMigratedChatModels } from './migrationUtils' * - Add following models: * - gpt-5.2 * - gpt-4.1-mini + * - gemini-3-pro-preview + * - gemini-3-flash-preview * - Update following models: * - claude-opus-4.1 -> claude-opus-4.5 * - Remove following models from defaults: @@ -15,6 +17,11 @@ import { getMigratedChatModels } from './migrationUtils' * - gpt-4o * - gpt-4o-mini * - o3 + * - gemini-2.5-pro + * - gemini-2.5-flash + * - gemini-2.5-flash-lite + * - gemini-2.0-flash + * - gemini-2.0-flash-lite */ export const migrateFrom13To14: SettingMigration['migrate'] = (data) => { const newData = { ...data } @@ -94,32 +101,14 @@ export const DEFAULT_CHAT_MODELS_V14: DefaultChatModelsV14 = [ { providerType: 'gemini', providerId: 'gemini', - id: 'gemini-2.5-pro', - model: 'gemini-2.5-pro', + id: 'gemini-3-pro-preview', + model: 'gemini-3-pro-preview', }, { providerType: 'gemini', providerId: 'gemini', - id: 'gemini-2.5-flash', - model: 'gemini-2.5-flash', - }, - { - providerType: 'gemini', - providerId: 'gemini', - id: 'gemini-2.5-flash-lite', - model: 'gemini-2.5-flash-lite', - }, - { - providerType: 'gemini', - providerId: 'gemini', - id: 'gemini-2.0-flash', - model: 'gemini-2.0-flash', - }, - { - providerType: 'gemini', - providerId: 'gemini', - id: 'gemini-2.0-flash-lite', - model: 'gemini-2.0-flash-lite', + id: 'gemini-3-flash-preview', + model: 'gemini-3-flash-preview', }, { providerType: 'deepseek', From 061ea9595f41a3b179589e5e48fd863b32fc9d35 Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:59:59 +0900 Subject: [PATCH 09/15] feat: Update Google AI integration and enhance chat message handling - Replaced the deprecated '@google/generative-ai' package with '@google/genai' to support the latest models and features. - Updated the GeminiProvider class to utilize the new API structure and methods from '@google/genai'. - Enhanced chat message serialization and deserialization to include provider metadata. - Added support for thought signatures in function calls to improve response handling. - Updated related types and utility functions to accommodate changes in the new API. --- package-lock.json | 712 +++++++++++++++++++++++++++- package.json | 4 +- src/core/llm/gemini.ts | 264 ++++++----- src/hooks/useChatHistory.ts | 2 + src/types/chat.ts | 4 +- src/types/llm/request.ts | 7 + src/types/llm/response.ts | 8 + src/utils/chat/promptGenerator.ts | 1 + src/utils/chat/responseGenerator.ts | 4 + 9 files changed, 871 insertions(+), 135 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74d3c554..5ad3edc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@electric-sql/pglite": "0.2.12", - "@google/generative-ai": "^0.24.0", + "@google/genai": "^1.35.0", "@lexical/clipboard": "^0.17.1", "@lexical/react": "^0.17.1", "@modelcontextprotocol/sdk": "^1.9.0", @@ -1251,12 +1251,37 @@ "version": "0.2.8", "license": "MIT" }, - "node_modules/@google/generative-ai": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz", - "integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==", + "node_modules/@google/genai": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.35.0.tgz", + "integrity": "sha512-ZC1d0PSM5eS73BpbVIgL3ZsmXeMKLVJurxzww1Z9axy3B2eUB3ioEytbQt4Qu0Od6qPluKrTDew9pSi9kEuPaw==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.24.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.8.tgz", + "integrity": "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" } }, "node_modules/@humanwhocodes/config-array": { @@ -1311,6 +1336,102 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -2015,23 +2136,82 @@ "peer": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz", - "integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==", + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", + "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", + "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" } }, "node_modules/@nodelib/fs.scandir": { @@ -2066,6 +2246,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", "license": "MIT" @@ -3224,6 +3414,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/agentkeepalive": { "version": "4.5.0", "license": "MIT", @@ -3249,6 +3448,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "dev": true, @@ -3276,7 +3514,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3284,7 +3521,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3638,6 +3874,15 @@ } ] }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -3726,6 +3971,12 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "dev": true, @@ -3949,7 +4200,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3960,7 +4210,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -4084,6 +4333,15 @@ "version": "3.1.3", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "dev": true, @@ -4859,6 +5117,21 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4896,7 +5169,6 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -6100,7 +6372,6 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -6139,6 +6410,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.17.1", "dev": true, @@ -6166,6 +6453,38 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "dev": true, @@ -6264,6 +6583,34 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "license": "MIT", @@ -6297,6 +6644,18 @@ "node": ">= 12.20" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6372,6 +6731,103 @@ "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==" }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gaxios/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "dev": true, @@ -6576,6 +7032,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -6617,6 +7100,19 @@ "undici-types": "~5.26.4" } }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "dev": true, @@ -6794,6 +7290,16 @@ "node": "*" } }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "dev": true, @@ -6822,6 +7328,19 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "license": "Apache-2.0", @@ -7103,7 +7622,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7440,6 +7958,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.2", "dev": true, @@ -8095,6 +8628,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tiktoken": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.15.tgz", @@ -8128,6 +8670,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "dev": true, @@ -8143,6 +8694,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "dev": true, @@ -8181,6 +8738,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -9311,6 +9889,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.29.4", "dev": true, @@ -9707,6 +10294,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -9808,6 +10401,28 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -10473,6 +11088,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "dev": true, @@ -10979,7 +11603,21 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11084,7 +11722,19 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11906,6 +12556,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "license": "ISC" @@ -11926,8 +12594,6 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "optional": true, - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 3964d8b6..15fe7b5e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@electric-sql/pglite": "0.2.12", - "@google/generative-ai": "^0.24.0", + "@google/genai": "^1.35.0", "@lexical/clipboard": "^0.17.1", "@lexical/react": "^0.17.1", "@modelcontextprotocol/sdk": "^1.9.0", @@ -83,4 +83,4 @@ "vscode-diff": "^2.1.1", "zod": "^3.23.8" } -} \ No newline at end of file +} diff --git a/src/core/llm/gemini.ts b/src/core/llm/gemini.ts index 2e80b0f9..863a478e 100644 --- a/src/core/llm/gemini.ts +++ b/src/core/llm/gemini.ts @@ -1,15 +1,13 @@ import { Content, - EnhancedGenerateContentResponse, - FunctionCallPart, - Tool as GeminiTool, - GenerateContentResult, - GenerateContentStreamResult, - GoogleGenerativeAI, + FunctionCall, + GenerateContentResponse, + GoogleGenAI, Part, Schema, - SchemaType, -} from '@google/generative-ai' + Tool, + Type, +} from '@google/genai' import { v4 as uuidv4 } from 'uuid' import { ChatModel } from '../../types/chat-model.types' @@ -34,12 +32,6 @@ import { LLMRateLimitExceededException, } from './exception' -/** - * TODO: Consider future migration from '@google/generative-ai' to '@google/genai' (https://github.com/googleapis/js-genai) - * - Current '@google/generative-ai' library will not support newest models and features - * - Not migrating yet as '@google/genai' is still in preview status - */ - /** * Note on OpenAI Compatibility API: * Gemini provides an OpenAI-compatible endpoint (https://ai.google.dev/gemini-api/docs/openai) @@ -50,7 +42,7 @@ import { export class GeminiProvider extends BaseLLMProvider< Extract > { - private client: GoogleGenerativeAI + private client: GoogleGenAI private apiKey: string constructor(provider: Extract) { @@ -59,7 +51,7 @@ export class GeminiProvider extends BaseLLMProvider< throw new Error('Gemini does not support custom base URL') } - this.client = new GoogleGenerativeAI(provider.apiKey ?? '') + this.client = new GoogleGenAI({ apiKey: provider.apiKey ?? '' }) this.apiKey = provider.apiKey ?? '' } @@ -85,32 +77,24 @@ export class GeminiProvider extends BaseLLMProvider< : undefined try { - const model = this.client.getGenerativeModel({ + const result = await this.client.models.generateContent({ model: request.model, - generationConfig: { + contents: request.messages + .map((message) => GeminiProvider.parseRequestMessage(message)) + .filter((m): m is Content => m !== null), + config: { maxOutputTokens: request.max_tokens, temperature: request.temperature, topP: request.top_p, presencePenalty: request.presence_penalty, frequencyPenalty: request.frequency_penalty, - }, - systemInstruction: systemInstruction, - }) - - const result = await model.generateContent( - { systemInstruction: systemInstruction, - contents: request.messages - .map((message) => GeminiProvider.parseRequestMessage(message)) - .filter((m): m is Content => m !== null), + abortSignal: options?.signal, tools: request.tools?.map((tool) => GeminiProvider.parseRequestTool(tool), ), }, - { - signal: options?.signal, - }, - ) + }) const messageId = crypto.randomUUID() // Gemini does not return a message id return GeminiProvider.parseNonStreamingResponse( @@ -156,32 +140,24 @@ export class GeminiProvider extends BaseLLMProvider< : undefined try { - const model = this.client.getGenerativeModel({ + const stream = await this.client.models.generateContentStream({ model: request.model, - generationConfig: { + contents: request.messages + .map((message) => GeminiProvider.parseRequestMessage(message)) + .filter((m): m is Content => m !== null), + config: { maxOutputTokens: request.max_tokens, temperature: request.temperature, topP: request.top_p, presencePenalty: request.presence_penalty, frequencyPenalty: request.frequency_penalty, - }, - systemInstruction: systemInstruction, - }) - - const stream = await model.generateContentStream( - { systemInstruction: systemInstruction, - contents: request.messages - .map((message) => GeminiProvider.parseRequestMessage(message)) - .filter((m): m is Content => m !== null), + abortSignal: options?.signal, tools: request.tools?.map((tool) => GeminiProvider.parseRequestTool(tool), ), }, - { - signal: options?.signal, - }, - ) + }) const messageId = crypto.randomUUID() // Gemini does not return a message id return this.streamResponseGenerator(stream, request.model, messageId) @@ -202,11 +178,11 @@ export class GeminiProvider extends BaseLLMProvider< } private async *streamResponseGenerator( - stream: GenerateContentStreamResult, + stream: AsyncGenerator, model: string, messageId: string, ): AsyncIterable { - for await (const chunk of stream.stream) { + for await (const chunk of stream) { yield GeminiProvider.parseStreamingResponseChunk(chunk, model, messageId) } } @@ -245,28 +221,47 @@ export class GeminiProvider extends BaseLLMProvider< } } case 'assistant': { - const contentParts: Part[] = [ - ...(message.content === '' ? [] : [{ text: message.content }]), - ...(message.tool_calls?.map((toolCall): FunctionCallPart => { + const thoughtSignature = + message.providerMetadata?.gemini?.thoughtSignature + const hasToolCalls = message.tool_calls && message.tool_calls.length > 0 + + const contentParts: Part[] = [] + + // Add text content part + if (message.content !== '') { + // If no tool calls and we have a signature, attach it to the text part + if (!hasToolCalls && thoughtSignature) { + contentParts.push({ text: message.content, thoughtSignature }) + } else { + contentParts.push({ text: message.content }) + } + } + + // Add function call parts + if (message.tool_calls) { + message.tool_calls.forEach((toolCall, index) => { + let args: Record try { - const args = JSON.parse(toolCall.arguments ?? '{}') - return { - functionCall: { - name: toolCall.name, - args, - }, - } - } catch (error) { - // If the arguments are not valid JSON, return an empty object - return { - functionCall: { - name: toolCall.name, - args: {}, - }, - } + args = JSON.parse(toolCall.arguments ?? '{}') + } catch { + args = {} } - }) ?? []), - ] + + const part: Part = { + functionCall: { + name: toolCall.name, + args, + }, + } + + // Attach signature to the first function call part + if (index === 0 && thoughtSignature) { + part.thoughtSignature = thoughtSignature + } + + contentParts.push(part) + }) + } if (contentParts.length === 0) { return null @@ -294,65 +289,66 @@ export class GeminiProvider extends BaseLLMProvider< } static parseNonStreamingResponse( - response: GenerateContentResult, + response: GenerateContentResponse, model: string, messageId: string, ): LLMResponseNonStreaming { + const thoughtSignature = GeminiProvider.extractThoughtSignature( + response.candidates?.[0]?.content?.parts, + ) + return { id: messageId, choices: [ { - finish_reason: - response.response.candidates?.[0]?.finishReason ?? null, + finish_reason: response.candidates?.[0]?.finishReason ?? null, message: { - content: response.response.text(), + content: response.text ?? '', role: 'assistant', - tool_calls: response.response.functionCalls()?.map((f) => ({ - id: uuidv4(), - type: 'function', - function: { - name: f.name, - arguments: JSON.stringify(f.args), - }, - })), + tool_calls: GeminiProvider.parseFunctionCalls( + response.functionCalls, + ), + providerMetadata: thoughtSignature + ? { gemini: { thoughtSignature } } + : undefined, }, }, ], created: Date.now(), model: model, object: 'chat.completion', - usage: response.response.usageMetadata + usage: response.usageMetadata ? { - prompt_tokens: response.response.usageMetadata.promptTokenCount, - completion_tokens: - response.response.usageMetadata.candidatesTokenCount, - total_tokens: response.response.usageMetadata.totalTokenCount, + prompt_tokens: response.usageMetadata.promptTokenCount ?? 0, + completion_tokens: response.usageMetadata.candidatesTokenCount ?? 0, + total_tokens: response.usageMetadata.totalTokenCount ?? 0, } : undefined, } } static parseStreamingResponseChunk( - chunk: EnhancedGenerateContentResponse, + chunk: GenerateContentResponse, model: string, messageId: string, ): LLMResponseStreaming { + const thoughtSignature = GeminiProvider.extractThoughtSignature( + chunk.candidates?.[0]?.content?.parts, + ) + return { id: messageId, choices: [ { finish_reason: chunk.candidates?.[0]?.finishReason ?? null, delta: { - content: chunk.text(), - tool_calls: chunk.functionCalls()?.map((f, index) => ({ - index, - id: uuidv4(), - type: 'function', - function: { - name: f.name, - arguments: JSON.stringify(f.args), - }, - })), + content: chunk.text, + tool_calls: GeminiProvider.parseFunctionCallsForStreaming( + chunk.functionCalls, + ), + providerMetadata: thoughtSignature + ? { gemini: { thoughtSignature } } + : undefined, }, }, ], @@ -361,14 +357,66 @@ export class GeminiProvider extends BaseLLMProvider< object: 'chat.completion.chunk', usage: chunk.usageMetadata ? { - prompt_tokens: chunk.usageMetadata.promptTokenCount, - completion_tokens: chunk.usageMetadata.candidatesTokenCount, - total_tokens: chunk.usageMetadata.totalTokenCount, + prompt_tokens: chunk.usageMetadata.promptTokenCount ?? 0, + completion_tokens: chunk.usageMetadata.candidatesTokenCount ?? 0, + total_tokens: chunk.usageMetadata.totalTokenCount ?? 0, } : undefined, } } + private static parseFunctionCalls(functionCalls: FunctionCall[] | undefined) { + return functionCalls?.map((f) => ({ + id: f.id ?? uuidv4(), + type: 'function' as const, + function: { + name: f.name ?? '', + arguments: JSON.stringify(f.args ?? {}), + }, + })) + } + + private static parseFunctionCallsForStreaming( + functionCalls: FunctionCall[] | undefined, + ) { + return functionCalls?.map((f, index) => ({ + index, + id: f.id ?? uuidv4(), + type: 'function' as const, + function: { + name: f.name ?? '', + arguments: JSON.stringify(f.args ?? {}), + }, + })) + } + + /** + * Extracts the thought signature from Gemini response parts. + * Per Gemini docs: + * - With function calls: signature is on the first functionCall part + * - Without function calls: signature is on the last part + */ + private static extractThoughtSignature( + parts: Part[] | undefined, + ): string | undefined { + if (!parts || parts.length === 0) { + return undefined + } + + // Check if there are function calls + const hasFunctionCalls = parts.some((part) => part.functionCall) + + if (hasFunctionCalls) { + // Signature is on the first function call part + const firstFcPart = parts.find((part) => part.functionCall) + return firstFcPart?.thoughtSignature + } else { + // Signature is on the last part + const lastPart = parts[parts.length - 1] + return lastPart?.thoughtSignature + } + } + private static removeAdditionalProperties(schema: unknown): unknown { // TODO: Remove this function when Gemini supports additionalProperties field in JSON schema if (typeof schema !== 'object' || schema === null) { @@ -392,7 +440,7 @@ export class GeminiProvider extends BaseLLMProvider< ) } - private static parseRequestTool(tool: RequestTool): GeminiTool { + private static parseRequestTool(tool: RequestTool): Tool { // Gemini does not support additionalProperties field in JSON schema, so we need to clean it const cleanedParameters = this.removeAdditionalProperties( tool.function.parameters, @@ -404,11 +452,8 @@ export class GeminiProvider extends BaseLLMProvider< name: tool.function.name, description: tool.function.description, parameters: { - type: SchemaType.OBJECT, - properties: (cleanedParameters.properties ?? {}) as Record< - string, - Schema - >, + type: Type.OBJECT, + properties: cleanedParameters.properties as Record, }, }, ], @@ -440,10 +485,11 @@ export class GeminiProvider extends BaseLLMProvider< } try { - const response = await this.client - .getGenerativeModel({ model: model }) - .embedContent(text) - return response.embedding.values + const response = await this.client.models.embedContent({ + model: model, + contents: text, + }) + return response.embeddings?.[0]?.values ?? [] } catch (error) { if (error.status === 429) { throw new LLMRateLimitExceededException( diff --git a/src/hooks/useChatHistory.ts b/src/hooks/useChatHistory.ts index 6c14a34c..0019e112 100644 --- a/src/hooks/useChatHistory.ts +++ b/src/hooks/useChatHistory.ts @@ -147,6 +147,7 @@ const serializeChatMessage = (message: ChatMessage): SerializedChatMessage => { toolCallRequests: message.toolCallRequests, id: message.id, metadata: message.metadata, + providerMetadata: message.providerMetadata, } case 'tool': return { @@ -183,6 +184,7 @@ const deserializeChatMessage = ( toolCallRequests: message.toolCallRequests, id: message.id, metadata: message.metadata, + providerMetadata: message.providerMetadata, } case 'tool': return { diff --git a/src/types/chat.ts b/src/types/chat.ts index a682bbe8..2bbae43e 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -3,7 +3,7 @@ import { SerializedEditorState } from 'lexical' import { SelectEmbedding } from '../database/schema' import { ChatModel } from './chat-model.types' -import { ContentPart } from './llm/request' +import { ContentPart, ProviderMetadata } from './llm/request' import { Annotation, ResponseUsage } from './llm/response' import { Mentionable, SerializedMentionable } from './mentionable' import { ToolCallRequest, ToolCallResponse } from './tool-call.types' @@ -29,6 +29,7 @@ export type ChatAssistantMessage = { usage?: ResponseUsage model?: ChatModel // TODO: migrate legacy data to new model type } + providerMetadata?: ProviderMetadata } export type ChatToolMessage = { role: 'tool' @@ -70,6 +71,7 @@ export type SerializedChatAssistantMessage = { usage?: ResponseUsage model?: ChatModel // TODO: migrate legacy data to new model type } + providerMetadata?: ProviderMetadata } export type SerializedChatToolMessage = { role: 'tool' diff --git a/src/types/llm/request.ts b/src/types/llm/request.ts index 20b2030f..69079a7b 100644 --- a/src/types/llm/request.ts +++ b/src/types/llm/request.ts @@ -65,10 +65,17 @@ type RequestUserMessage = { role: 'user' content: string | ContentPart[] } +export type ProviderMetadata = { + gemini?: { + thoughtSignature?: string + } +} + type RequestAssistantMessage = { role: 'assistant' content: string tool_calls?: ToolCallRequest[] + providerMetadata?: ProviderMetadata } type RequestToolMessage = { role: 'tool' diff --git a/src/types/llm/response.ts b/src/types/llm/response.ts index 16f72908..484da20e 100644 --- a/src/types/llm/response.ts +++ b/src/types/llm/response.ts @@ -27,6 +27,12 @@ export type ResponseUsage = { total_tokens: number } +export type ResponseProviderMetadata = { + gemini?: { + thoughtSignature?: string + } +} + type NonStreamingChoice = { finish_reason: string | null // Depends on the model. Ex: 'stop' | 'length' | 'content_filter' | 'tool_calls' | 'function_call' message: { @@ -35,6 +41,7 @@ type NonStreamingChoice = { role: string annotations?: Annotation[] tool_calls?: ToolCall[] + providerMetadata?: ResponseProviderMetadata } error?: Error } @@ -47,6 +54,7 @@ type StreamingChoice = { role?: string annotations?: Annotation[] tool_calls?: ToolCallDelta[] + providerMetadata?: ResponseProviderMetadata } error?: Error } diff --git a/src/utils/chat/promptGenerator.ts b/src/utils/chat/promptGenerator.ts index 0e0ffe13..44714fcd 100644 --- a/src/utils/chat/promptGenerator.ts +++ b/src/utils/chat/promptGenerator.ts @@ -207,6 +207,7 @@ ${message.annotations ...(citationContent ? [citationContent] : []), ].join('\n'), tool_calls: message.toolCallRequests, + providerMetadata: message.providerMetadata, }, ] } diff --git a/src/utils/chat/responseGenerator.ts b/src/utils/chat/responseGenerator.ts index 6c552dbf..7dff5f45 100644 --- a/src/utils/chat/responseGenerator.ts +++ b/src/utils/chat/responseGenerator.ts @@ -279,6 +279,8 @@ export class ResponseGenerator { }) } + const providerMetadata = chunk.choices[0]?.delta?.providerMetadata + this.updateResponseMessages((messages) => messages.map((message) => message.id === responseMessageId && message.role === 'assistant' @@ -296,6 +298,8 @@ export class ResponseGenerator { ...message.metadata, usage: chunk.usage ?? message.metadata?.usage, }, + // Keep the first providerMetadata received (signature is sent once) + providerMetadata: message.providerMetadata ?? providerMetadata, } : message, ), From 6e66c34ec23c7f1d2fc0aca5c05184c6c8792bff Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:41:33 +0900 Subject: [PATCH 10/15] feat: Enhance Gemini model settings and thinking capabilities - Added comprehensive settings for the Gemini model, including thinking level, budget, and thought summaries. - Implemented a new ThinkingConfig structure to manage thinking settings based on model type. - Updated the GeminiProvider to extract and include reasoning in responses. - Enhanced the AddChatModelModal to support the new provider settings for Gemini. --- .../settings/modals/AddChatModelModal.tsx | 14 +- .../sections/models/ChatModelSettings.tsx | 152 ++++++++++++++++++ src/core/llm/gemini.ts | 76 ++++++++- src/types/chat-model.types.ts | 13 ++ 4 files changed, 244 insertions(+), 11 deletions(-) diff --git a/src/components/settings/modals/AddChatModelModal.tsx b/src/components/settings/modals/AddChatModelModal.tsx index 5f11edf3..f96c82ce 100644 --- a/src/components/settings/modals/AddChatModelModal.tsx +++ b/src/components/settings/modals/AddChatModelModal.tsx @@ -103,11 +103,15 @@ function AddChatModelModalComponent({ new Notice(`Provider with ID ${value} not found`) return } - setFormData((prev) => ({ - ...prev, - providerId: value, - providerType: provider.type, - })) + // Cast required because we're changing the discriminant field + setFormData( + (prev) => + ({ + ...prev, + providerId: value, + providerType: provider.type, + }) as ChatModel, + ) }} /> diff --git a/src/components/settings/sections/models/ChatModelSettings.tsx b/src/components/settings/sections/models/ChatModelSettings.tsx index ab50ecf3..e084d3a1 100644 --- a/src/components/settings/sections/models/ChatModelSettings.tsx +++ b/src/components/settings/sections/models/ChatModelSettings.tsx @@ -225,6 +225,158 @@ const MODEL_SETTINGS_REGISTRY: ModelSettingsRegistry[] = [ }, }, + /** + * Gemini model settings + * + * For thinking, see: + * @see https://ai.google.dev/gemini-api/docs/thinking + */ + { + check: (model) => model.providerType === 'gemini', + SettingsComponent: (props: SettingsComponentProps) => { + const { model, plugin, onClose } = props + const typedModel = model as ChatModel & { providerType: 'gemini' } + const [thinkingEnabled, setThinkingEnabled] = useState( + typedModel.thinking?.enabled ?? false, + ) + const [controlMode, setControlMode] = useState<'level' | 'budget'>( + typedModel.thinking?.control_mode ?? 'level', + ) + const [thinkingLevel, setThinkingLevel] = useState( + String(typedModel.thinking?.thinking_level ?? 'high'), + ) + const [thinkingBudget, setThinkingBudget] = useState( + String(typedModel.thinking?.thinking_budget ?? -1), + ) + const [includeThoughts, setIncludeThoughts] = useState( + Boolean(typedModel.thinking?.include_thoughts ?? false), + ) + + const handleSubmit = async () => { + let parsedBudget: number | undefined + if (controlMode === 'budget') { + parsedBudget = parseInt(thinkingBudget, 10) + if (isNaN(parsedBudget)) { + new Notice('Please enter a valid number for thinking budget') + return + } + } + + const updatedModel = { + ...typedModel, + thinking: { + enabled: thinkingEnabled, + control_mode: controlMode, + thinking_level: + controlMode === 'level' + ? (thinkingLevel as 'minimal' | 'low' | 'medium' | 'high') + : undefined, + thinking_budget: + controlMode === 'budget' ? parsedBudget : undefined, + include_thoughts: includeThoughts, + }, + } + + const validationResult = chatModelSchema.safeParse(updatedModel) + if (!validationResult.success) { + new Notice( + validationResult.error.issues.map((v) => v.message).join('\n'), + ) + return + } + + await plugin.setSettings({ + ...plugin.settings, + chatModels: plugin.settings.chatModels.map((m) => + m.id === model.id ? updatedModel : m, + ), + }) + onClose() + } + + return ( + <> + + setThinkingEnabled(value)} + /> + + {thinkingEnabled && ( + <> + + + setControlMode(value as 'level' | 'budget') + } + /> + + {controlMode === 'level' && ( + + setThinkingLevel(value)} + /> + + )} + {controlMode === 'budget' && ( + + setThinkingBudget(value)} + type="number" + /> + + )} + + setIncludeThoughts(value)} + /> + + + )} + + + + + + + ) + }, + }, + // Perplexity settings { check: (model) => diff --git a/src/core/llm/gemini.ts b/src/core/llm/gemini.ts index 863a478e..170507ce 100644 --- a/src/core/llm/gemini.ts +++ b/src/core/llm/gemini.ts @@ -5,6 +5,8 @@ import { GoogleGenAI, Part, Schema, + ThinkingConfig, + ThinkingLevel, Tool, Type, } from '@google/genai' @@ -93,6 +95,7 @@ export class GeminiProvider extends BaseLLMProvider< tools: request.tools?.map((tool) => GeminiProvider.parseRequestTool(tool), ), + thinkingConfig: GeminiProvider.buildThinkingConfig(model), }, }) @@ -156,6 +159,7 @@ export class GeminiProvider extends BaseLLMProvider< tools: request.tools?.map((tool) => GeminiProvider.parseRequestTool(tool), ), + thinkingConfig: GeminiProvider.buildThinkingConfig(model), }, }) @@ -293,9 +297,9 @@ export class GeminiProvider extends BaseLLMProvider< model: string, messageId: string, ): LLMResponseNonStreaming { - const thoughtSignature = GeminiProvider.extractThoughtSignature( - response.candidates?.[0]?.content?.parts, - ) + const parts = response.candidates?.[0]?.content?.parts + const thoughtSignature = GeminiProvider.extractThoughtSignature(parts) + const reasoning = GeminiProvider.extractThoughtSummaries(parts) return { id: messageId, @@ -304,6 +308,7 @@ export class GeminiProvider extends BaseLLMProvider< finish_reason: response.candidates?.[0]?.finishReason ?? null, message: { content: response.text ?? '', + reasoning: reasoning, role: 'assistant', tool_calls: GeminiProvider.parseFunctionCalls( response.functionCalls, @@ -332,9 +337,9 @@ export class GeminiProvider extends BaseLLMProvider< model: string, messageId: string, ): LLMResponseStreaming { - const thoughtSignature = GeminiProvider.extractThoughtSignature( - chunk.candidates?.[0]?.content?.parts, - ) + const parts = chunk.candidates?.[0]?.content?.parts + const thoughtSignature = GeminiProvider.extractThoughtSignature(parts) + const reasoning = GeminiProvider.extractThoughtSummaries(parts) return { id: messageId, @@ -343,6 +348,7 @@ export class GeminiProvider extends BaseLLMProvider< finish_reason: chunk.candidates?.[0]?.finishReason ?? null, delta: { content: chunk.text, + reasoning: reasoning, tool_calls: GeminiProvider.parseFunctionCallsForStreaming( chunk.functionCalls, ), @@ -417,6 +423,25 @@ export class GeminiProvider extends BaseLLMProvider< } } + /** + * Extracts thought summaries from Gemini response parts. + * Thought summaries are parts with thought: true and contain reasoning text. + */ + private static extractThoughtSummaries( + parts: Part[] | undefined, + ): string | undefined { + if (!parts || parts.length === 0) { + return undefined + } + + const thoughtParts = parts.filter((part) => part.thought && part.text) + if (thoughtParts.length === 0) { + return undefined + } + + return thoughtParts.map((part) => part.text).join('') + } + private static removeAdditionalProperties(schema: unknown): unknown { // TODO: Remove this function when Gemini supports additionalProperties field in JSON schema if (typeof schema !== 'object' || schema === null) { @@ -477,6 +502,45 @@ export class GeminiProvider extends BaseLLMProvider< } } + private static readonly THINKING_LEVEL_MAP: Record = { + minimal: ThinkingLevel.MINIMAL, + low: ThinkingLevel.LOW, + medium: ThinkingLevel.MEDIUM, + high: ThinkingLevel.HIGH, + } + + /** + * Builds the thinking config for Gemini API based on model settings. + * - Gemini 3 models use thinkingLevel + * - Gemini 2.5 models use thinkingBudget + */ + private static buildThinkingConfig( + model: ChatModel & { providerType: 'gemini' }, + ): ThinkingConfig | undefined { + if (!model.thinking?.enabled) { + return undefined + } + + const config: ThinkingConfig = {} + + if (model.thinking.thinking_level) { + const level = this.THINKING_LEVEL_MAP[model.thinking.thinking_level] + if (level) { + config.thinkingLevel = level + } + } + + if (model.thinking.thinking_budget !== undefined) { + config.thinkingBudget = model.thinking.thinking_budget + } + + if (model.thinking.include_thoughts) { + config.includeThoughts = model.thinking.include_thoughts + } + + return config + } + async getEmbedding(model: string, text: string): Promise { if (!this.apiKey) { throw new LLMAPIKeyNotSetException( diff --git a/src/types/chat-model.types.ts b/src/types/chat-model.types.ts index 25afa7f4..9a95c72c 100644 --- a/src/types/chat-model.types.ts +++ b/src/types/chat-model.types.ts @@ -49,6 +49,19 @@ export const chatModelSchema = z.discriminatedUnion('providerType', [ z.object({ providerType: z.literal('gemini'), ...baseChatModelSchema.shape, + thinking: z + .object({ + enabled: z.boolean(), + // 'level' for Gemini 3 models, 'budget' for Gemini 2.5 models + control_mode: z.enum(['level', 'budget']).optional(), + // For Gemini 3 models + thinking_level: z.enum(['minimal', 'low', 'medium', 'high']).optional(), + // For Gemini 2.5 models: -1 for dynamic, 0 to disable, or specific token count + thinking_budget: z.number().optional(), + // Return thought summaries in response + include_thoughts: z.boolean().optional(), + }) + .optional(), }), z.object({ providerType: z.literal('groq'), From 349b628a1595f700b1124cfa53ba4f919e8a6ca6 Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:44:46 +0900 Subject: [PATCH 11/15] feat: Add xAI provider and associated models - Introduced the xAI provider with necessary configurations and API integration. - Added new chat models 'grok-4-1-fast' and 'grok-4-1-fast-non-reasoning' to the default settings. - Updated migration logic to include the xAI provider and new models. - Enhanced pricing calculations to support xAI model costs. - Updated schemas to accommodate the new xAI provider type. --- src/constants.ts | 29 ++++++++++ src/core/llm/manager.ts | 4 ++ src/core/llm/xaiProvider.ts | 65 ++++++++++++++++++++++ src/settings/schema/migrations/13_to_14.ts | 37 +++++++++++- src/types/chat-model.types.ts | 4 ++ src/types/embedding-model.types.ts | 4 ++ src/types/provider.types.ts | 4 ++ src/utils/llm/price-calculator.ts | 16 +++++- 8 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 src/core/llm/xaiProvider.ts diff --git a/src/constants.ts b/src/constants.ts index db0de6c3..fc36e21f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -109,6 +109,14 @@ export const PROVIDER_TYPES_INFO = { supportEmbedding: false, additionalSettings: [], }, + xai: { + label: 'xAI', + defaultProviderId: 'xai', + requireApiKey: true, + requireBaseUrl: false, + supportEmbedding: false, + additionalSettings: [], + }, 'azure-openai': { label: 'Azure OpenAI', defaultProviderId: null, // no default provider for this type @@ -218,6 +226,10 @@ export const DEFAULT_PROVIDERS: readonly LLMProvider[] = [ type: 'morph', id: PROVIDER_TYPES_INFO.morph.defaultProviderId, }, + { + type: 'xai', + id: PROVIDER_TYPES_INFO.xai.defaultProviderId, + }, ] /** @@ -347,6 +359,18 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ id: 'morph-v0', model: 'morph-v0', }, + { + providerType: 'xai', + providerId: PROVIDER_TYPES_INFO.xai.defaultProviderId, + id: 'grok-4-1-fast', + model: 'grok-4-1-fast', + }, + { + providerType: 'xai', + providerId: PROVIDER_TYPES_INFO.xai.defaultProviderId, + id: 'grok-4-1-fast-non-reasoning', + model: 'grok-4-1-fast-non-reasoning', + }, ] /** @@ -437,3 +461,8 @@ export const ANTHROPIC_PRICES: Record = { // Gemini is currently free for low rate limits export const GEMINI_PRICES: Record = {} + +export const XAI_PRICES: Record = { + 'grok-4-1-fast': { input: 0.2, output: 0.5 }, + 'grok-4-1-fast-non-reasoning': { input: 0.2, output: 0.5 }, +} diff --git a/src/core/llm/manager.ts b/src/core/llm/manager.ts index b5858952..6aaeb7ad 100644 --- a/src/core/llm/manager.ts +++ b/src/core/llm/manager.ts @@ -17,6 +17,7 @@ import { OpenAIAuthenticatedProvider } from './openai' import { OpenAICompatibleProvider } from './openaiCompatibleProvider' import { OpenRouterProvider } from './openRouterProvider' import { PerplexityProvider } from './perplexityProvider' +import { XaiProvider } from './xaiProvider' /* * OpenAI, OpenAI-compatible, and Anthropic providers include token usage statistics @@ -70,6 +71,9 @@ export function getProviderClient({ case 'morph': { return new MorphProvider(provider) } + case 'xai': { + return new XaiProvider(provider) + } case 'azure-openai': { return new AzureOpenAIProvider(provider) } diff --git a/src/core/llm/xaiProvider.ts b/src/core/llm/xaiProvider.ts new file mode 100644 index 00000000..b2b6fe6f --- /dev/null +++ b/src/core/llm/xaiProvider.ts @@ -0,0 +1,65 @@ +import OpenAI from 'openai' + +import { ChatModel } from '../../types/chat-model.types' +import { + LLMOptions, + LLMRequestNonStreaming, + LLMRequestStreaming, +} from '../../types/llm/request' +import { + LLMResponseNonStreaming, + LLMResponseStreaming, +} from '../../types/llm/response' +import { LLMProvider } from '../../types/provider.types' + +import { BaseLLMProvider } from './base' +import { OpenAIMessageAdapter } from './openaiMessageAdapter' + +export class XaiProvider extends BaseLLMProvider< + Extract +> { + private adapter: OpenAIMessageAdapter + private client: OpenAI + + constructor(provider: Extract) { + super(provider) + this.adapter = new OpenAIMessageAdapter() + this.client = new OpenAI({ + apiKey: provider.apiKey ?? '', + baseURL: provider.baseUrl + ? provider.baseUrl.replace(/\/+$/, '') + : 'https://api.x.ai/v1', + dangerouslyAllowBrowser: true, + }) + } + + async generateResponse( + model: ChatModel, + request: LLMRequestNonStreaming, + options?: LLMOptions, + ): Promise { + if (model.providerType !== 'xai') { + throw new Error('Model is not an xAI model') + } + + return this.adapter.generateResponse(this.client, request, options) + } + + async streamResponse( + model: ChatModel, + request: LLMRequestStreaming, + options?: LLMOptions, + ): Promise> { + if (model.providerType !== 'xai') { + throw new Error('Model is not an xAI model') + } + + return this.adapter.streamResponse(this.client, request, options) + } + + async getEmbedding(_model: string, _text: string): Promise { + throw new Error( + `Provider ${String(this.provider.id)} does not support embeddings. Please use a different provider.`, + ) + } +} diff --git a/src/settings/schema/migrations/13_to_14.ts b/src/settings/schema/migrations/13_to_14.ts index e79e22ed..8ab5d55b 100644 --- a/src/settings/schema/migrations/13_to_14.ts +++ b/src/settings/schema/migrations/13_to_14.ts @@ -1,22 +1,25 @@ import { SettingMigration } from '../setting.types' -import { getMigratedChatModels } from './migrationUtils' +import { getMigratedChatModels, getMigratedProviders } from './migrationUtils' /** * Migration from version 13 to version 14 + * - Add xai provider * - Add following models: * - gpt-5.2 * - gpt-4.1-mini + * - claude-opus-4.5 * - gemini-3-pro-preview * - gemini-3-flash-preview - * - Update following models: - * - claude-opus-4.1 -> claude-opus-4.5 + * - grok-4-1-fast + * - grok-4-1-fast-non-reasoning * - Remove following models from defaults: * - gpt-5.1 * - gpt-5-nano * - gpt-4o * - gpt-4o-mini * - o3 + * - claude-opus-4.1 * - gemini-2.5-pro * - gemini-2.5-flash * - gemini-2.5-flash-lite @@ -27,11 +30,27 @@ export const migrateFrom13To14: SettingMigration['migrate'] = (data) => { const newData = { ...data } newData.version = 14 + newData.providers = getMigratedProviders(newData, DEFAULT_PROVIDERS_V14) newData.chatModels = getMigratedChatModels(newData, DEFAULT_CHAT_MODELS_V14) return newData } +const DEFAULT_PROVIDERS_V14 = [ + { type: 'openai', id: 'openai' }, + { type: 'anthropic', id: 'anthropic' }, + { type: 'gemini', id: 'gemini' }, + { type: 'deepseek', id: 'deepseek' }, + { type: 'perplexity', id: 'perplexity' }, + { type: 'groq', id: 'groq' }, + { type: 'mistral', id: 'mistral' }, + { type: 'openrouter', id: 'openrouter' }, + { type: 'ollama', id: 'ollama' }, + { type: 'lm-studio', id: 'lm-studio' }, + { type: 'morph', id: 'morph' }, + { type: 'xai', id: 'xai' }, +] as const + type DefaultChatModelsV14 = { id: string providerType: string @@ -173,4 +192,16 @@ export const DEFAULT_CHAT_MODELS_V14: DefaultChatModelsV14 = [ id: 'morph-v0', model: 'morph-v0', }, + { + providerType: 'xai', + providerId: 'xai', + id: 'grok-4-1-fast', + model: 'grok-4-1-fast', + }, + { + providerType: 'xai', + providerId: 'xai', + id: 'grok-4-1-fast-non-reasoning', + model: 'grok-4-1-fast-non-reasoning', + }, ] diff --git a/src/types/chat-model.types.ts b/src/types/chat-model.types.ts index 9a95c72c..a549caf9 100644 --- a/src/types/chat-model.types.ts +++ b/src/types/chat-model.types.ts @@ -100,6 +100,10 @@ export const chatModelSchema = z.discriminatedUnion('providerType', [ providerType: z.literal('morph'), ...baseChatModelSchema.shape, }), + z.object({ + providerType: z.literal('xai'), + ...baseChatModelSchema.shape, + }), z.object({ providerType: z.literal('azure-openai'), ...baseChatModelSchema.shape, diff --git a/src/types/embedding-model.types.ts b/src/types/embedding-model.types.ts index 64e7a1b1..b98da59a 100644 --- a/src/types/embedding-model.types.ts +++ b/src/types/embedding-model.types.ts @@ -64,6 +64,10 @@ export const embeddingModelSchema = z.discriminatedUnion('providerType', [ providerType: z.literal('morph'), ...baseEmbeddingModelSchema.shape, }), + z.object({ + providerType: z.literal('xai'), + ...baseEmbeddingModelSchema.shape, + }), z.object({ providerType: z.literal('azure-openai'), ...baseEmbeddingModelSchema.shape, diff --git a/src/types/provider.types.ts b/src/types/provider.types.ts index a9b193f4..ece01650 100644 --- a/src/types/provider.types.ts +++ b/src/types/provider.types.ts @@ -59,6 +59,10 @@ export const llmProviderSchema = z.discriminatedUnion('type', [ type: z.literal('morph'), ...baseLlmProviderSchema.shape, }), + z.object({ + type: z.literal('xai'), + ...baseLlmProviderSchema.shape, + }), z.object({ type: z.literal('azure-openai'), ...baseLlmProviderSchema.shape, diff --git a/src/utils/llm/price-calculator.ts b/src/utils/llm/price-calculator.ts index 5d7cf315..037ba110 100644 --- a/src/utils/llm/price-calculator.ts +++ b/src/utils/llm/price-calculator.ts @@ -1,4 +1,9 @@ -import { ANTHROPIC_PRICES, GEMINI_PRICES, OPENAI_PRICES } from '../../constants' +import { + ANTHROPIC_PRICES, + GEMINI_PRICES, + OPENAI_PRICES, + XAI_PRICES, +} from '../../constants' import { ChatModel } from '../../types/chat-model.types' import { ResponseUsage } from '../../types/llm/response' @@ -38,6 +43,15 @@ export const calculateLLMCost = ({ 1_000_000 ) } + case 'xai': { + const modelPricing = XAI_PRICES[model.model] + if (!modelPricing) return null + return ( + (usage.prompt_tokens * modelPricing.input + + usage.completion_tokens * modelPricing.output) / + 1_000_000 + ) + } default: return null } From f532a2b4cd6e39c4c41522081352c2f0e858b348 Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:54:19 +0900 Subject: [PATCH 12/15] feat: Enhance DeepSeekMessageAdapter to support reasoning content - Updated DeepSeekMessageAdapter to include reasoning content in both non-streaming and streaming responses. - Introduced providerMetadata to store reasoning content for assistant messages during tool call iterations. - Refactored request and response types to accommodate new RequestProviderMetadata structure for DeepSeek integration. --- src/core/llm/deepseekMessageAdapter.ts | 79 +++++++++++++++++++------- src/types/chat.ts | 6 +- src/types/llm/request.ts | 7 ++- src/types/llm/response.ts | 3 + 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/core/llm/deepseekMessageAdapter.ts b/src/core/llm/deepseekMessageAdapter.ts index 079e17a9..a16ae3c6 100644 --- a/src/core/llm/deepseekMessageAdapter.ts +++ b/src/core/llm/deepseekMessageAdapter.ts @@ -1,8 +1,10 @@ import { ChatCompletion, ChatCompletionChunk, + ChatCompletionMessageParam, } from 'openai/resources/chat/completions' +import { RequestMessage } from '../../types/llm/request' import { LLMResponseNonStreaming, LLMResponseStreaming, @@ -13,6 +15,10 @@ import { OpenAIMessageAdapter } from './openaiMessageAdapter' /** * Adapter for DeepSeek's API that extends OpenAIMessageAdapter to handle the additional * 'reasoning_content' field in DeepSeek's response format while maintaining OpenAI compatibility. + * + * DeepSeek's thinking mode requires `reasoning_content` to be passed back in assistant messages + * during tool call iterations. This adapter stores reasoning in providerMetadata and injects it + * back into API requests. */ export class DeepSeekMessageAdapter extends OpenAIMessageAdapter { protected parseNonStreamingResponse( @@ -20,17 +26,23 @@ export class DeepSeekMessageAdapter extends OpenAIMessageAdapter { ): LLMResponseNonStreaming { return { id: response.id, - choices: response.choices.map((choice) => ({ - finish_reason: choice.finish_reason, - message: { - content: choice.message.content, - reasoning: ( - choice.message as unknown as { reasoning_content?: string } - ).reasoning_content, - role: choice.message.role, - tool_calls: choice.message.tool_calls, - }, - })), + choices: response.choices.map((choice) => { + const reasoningContent = ( + choice.message as unknown as { reasoning_content?: string } + ).reasoning_content + return { + finish_reason: choice.finish_reason, + message: { + content: choice.message.content, + reasoning: reasoningContent, + role: choice.message.role, + tool_calls: choice.message.tool_calls, + providerMetadata: reasoningContent + ? { deepseek: { reasoningContent } } + : undefined, + }, + } + }), created: response.created, model: response.model, object: 'chat.completion', @@ -44,16 +56,23 @@ export class DeepSeekMessageAdapter extends OpenAIMessageAdapter { ): LLMResponseStreaming { return { id: chunk.id, - choices: chunk.choices.map((choice) => ({ - finish_reason: choice.finish_reason ?? null, - delta: { - content: choice.delta.content ?? null, - reasoning: (choice.delta as unknown as { reasoning_content?: string }) - .reasoning_content, - role: choice.delta.role, - tool_calls: choice.delta.tool_calls, - }, - })), + choices: chunk.choices.map((choice) => { + const reasoningContent = ( + choice.delta as unknown as { reasoning_content?: string } + ).reasoning_content + return { + finish_reason: choice.finish_reason ?? null, + delta: { + content: choice.delta.content ?? null, + reasoning: reasoningContent, + role: choice.delta.role, + tool_calls: choice.delta.tool_calls, + providerMetadata: reasoningContent + ? { deepseek: { reasoningContent } } + : undefined, + }, + } + }), created: chunk.created, model: chunk.model, object: 'chat.completion.chunk', @@ -61,4 +80,22 @@ export class DeepSeekMessageAdapter extends OpenAIMessageAdapter { usage: chunk.usage ?? undefined, } } + + protected parseRequestMessage( + message: RequestMessage, + ): ChatCompletionMessageParam { + const baseMessage = super.parseRequestMessage(message) + + if ( + message.role === 'assistant' && + message.providerMetadata?.deepseek?.reasoningContent + ) { + return { + ...baseMessage, + reasoning_content: message.providerMetadata.deepseek.reasoningContent, + } as unknown as ChatCompletionMessageParam + } + + return baseMessage + } } diff --git a/src/types/chat.ts b/src/types/chat.ts index 2bbae43e..17173f18 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -3,7 +3,7 @@ import { SerializedEditorState } from 'lexical' import { SelectEmbedding } from '../database/schema' import { ChatModel } from './chat-model.types' -import { ContentPart, ProviderMetadata } from './llm/request' +import { ContentPart, RequestProviderMetadata } from './llm/request' import { Annotation, ResponseUsage } from './llm/response' import { Mentionable, SerializedMentionable } from './mentionable' import { ToolCallRequest, ToolCallResponse } from './tool-call.types' @@ -29,7 +29,7 @@ export type ChatAssistantMessage = { usage?: ResponseUsage model?: ChatModel // TODO: migrate legacy data to new model type } - providerMetadata?: ProviderMetadata + providerMetadata?: RequestProviderMetadata } export type ChatToolMessage = { role: 'tool' @@ -71,7 +71,7 @@ export type SerializedChatAssistantMessage = { usage?: ResponseUsage model?: ChatModel // TODO: migrate legacy data to new model type } - providerMetadata?: ProviderMetadata + providerMetadata?: RequestProviderMetadata } export type SerializedChatToolMessage = { role: 'tool' diff --git a/src/types/llm/request.ts b/src/types/llm/request.ts index 69079a7b..3f208a94 100644 --- a/src/types/llm/request.ts +++ b/src/types/llm/request.ts @@ -65,17 +65,20 @@ type RequestUserMessage = { role: 'user' content: string | ContentPart[] } -export type ProviderMetadata = { +export type RequestProviderMetadata = { gemini?: { thoughtSignature?: string } + deepseek?: { + reasoningContent?: string + } } type RequestAssistantMessage = { role: 'assistant' content: string tool_calls?: ToolCallRequest[] - providerMetadata?: ProviderMetadata + providerMetadata?: RequestProviderMetadata } type RequestToolMessage = { role: 'tool' diff --git a/src/types/llm/response.ts b/src/types/llm/response.ts index 484da20e..18b7b65d 100644 --- a/src/types/llm/response.ts +++ b/src/types/llm/response.ts @@ -31,6 +31,9 @@ export type ResponseProviderMetadata = { gemini?: { thoughtSignature?: string } + deepseek?: { + reasoningContent?: string + } } type NonStreamingChoice = { From 5237d8252936f2a50dc6c7ad42d67806924bf0be Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:47:19 +0900 Subject: [PATCH 13/15] feat: Refactor provider types and remove legacy models - Introduced the xAI provider and updated associated configurations. - Removed the Groq and Morph providers, converting their models to openai-compatible. - Updated migration logic to reflect these changes and ensure proper handling of existing models. - Enhanced schemas to accommodate the new provider types and updated chat model definitions. --- src/constants.ts | 141 +-- src/core/llm/groq.ts | 65 -- src/core/llm/manager.ts | 9 - src/core/llm/morphProvider.ts | 76 -- .../schema/migrations/13_to_14.test.ts | 1020 ++++++++++++++--- src/settings/schema/migrations/13_to_14.ts | 109 +- src/settings/schema/migrations/1_to_2.ts | 22 +- src/settings/schema/migrations/2_to_3.ts | 28 +- src/types/chat-model.types.ts | 20 +- src/types/embedding-model.types.ts | 22 +- src/types/provider.types.ts | 14 +- 11 files changed, 1015 insertions(+), 511 deletions(-) delete mode 100644 src/core/llm/groq.ts delete mode 100644 src/core/llm/morphProvider.ts diff --git a/src/constants.ts b/src/constants.ts index fc36e21f..e992870c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -45,41 +45,25 @@ export const PROVIDER_TYPES_INFO = { supportEmbedding: true, additionalSettings: [], }, - groq: { - label: 'Groq', - defaultProviderId: 'groq', + xai: { + label: 'xAI', + defaultProviderId: 'xai', requireApiKey: true, requireBaseUrl: false, supportEmbedding: false, additionalSettings: [], }, - openrouter: { - label: 'OpenRouter', - defaultProviderId: 'openrouter', + deepseek: { + label: 'DeepSeek', + defaultProviderId: 'deepseek', requireApiKey: true, requireBaseUrl: false, supportEmbedding: false, additionalSettings: [], }, - ollama: { - label: 'Ollama', - defaultProviderId: 'ollama', - requireApiKey: false, - requireBaseUrl: false, - supportEmbedding: true, - additionalSettings: [], - }, - 'lm-studio': { - label: 'LM Studio', - defaultProviderId: 'lm-studio', - requireApiKey: false, - requireBaseUrl: false, - supportEmbedding: true, - additionalSettings: [], - }, - deepseek: { - label: 'DeepSeek', - defaultProviderId: 'deepseek', + mistral: { + label: 'Mistral', + defaultProviderId: 'mistral', requireApiKey: true, requireBaseUrl: false, supportEmbedding: false, @@ -93,28 +77,28 @@ export const PROVIDER_TYPES_INFO = { supportEmbedding: false, additionalSettings: [], }, - mistral: { - label: 'Mistral', - defaultProviderId: 'mistral', + openrouter: { + label: 'OpenRouter', + defaultProviderId: 'openrouter', requireApiKey: true, requireBaseUrl: false, supportEmbedding: false, additionalSettings: [], }, - morph: { - label: 'Morph', - defaultProviderId: 'morph', - requireApiKey: true, + ollama: { + label: 'Ollama', + defaultProviderId: 'ollama', + requireApiKey: false, requireBaseUrl: false, - supportEmbedding: false, + supportEmbedding: true, additionalSettings: [], }, - xai: { - label: 'xAI', - defaultProviderId: 'xai', - requireApiKey: true, + 'lm-studio': { + label: 'LM Studio', + defaultProviderId: 'lm-studio', + requireApiKey: false, requireBaseUrl: false, - supportEmbedding: false, + supportEmbedding: true, additionalSettings: [], }, 'azure-openai': { @@ -195,21 +179,21 @@ export const DEFAULT_PROVIDERS: readonly LLMProvider[] = [ id: PROVIDER_TYPES_INFO.gemini.defaultProviderId, }, { - type: 'deepseek', - id: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, - }, - { - type: 'perplexity', - id: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, + type: 'xai', + id: PROVIDER_TYPES_INFO.xai.defaultProviderId, }, { - type: 'groq', - id: PROVIDER_TYPES_INFO.groq.defaultProviderId, + type: 'deepseek', + id: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, }, { type: 'mistral', id: PROVIDER_TYPES_INFO.mistral.defaultProviderId, }, + { + type: 'perplexity', + id: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, + }, { type: 'openrouter', id: PROVIDER_TYPES_INFO.openrouter.defaultProviderId, @@ -222,14 +206,6 @@ export const DEFAULT_PROVIDERS: readonly LLMProvider[] = [ type: 'lm-studio', id: PROVIDER_TYPES_INFO['lm-studio'].defaultProviderId, }, - { - type: 'morph', - id: PROVIDER_TYPES_INFO.morph.defaultProviderId, - }, - { - type: 'xai', - id: PROVIDER_TYPES_INFO.xai.defaultProviderId, - }, ] /** @@ -308,57 +284,6 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ id: 'deepseek-reasoner', model: 'deepseek-reasoner', }, - { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar-pro', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar-deep-research', - model: 'sonar-deep-research', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar-reasoning', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar-reasoning-pro', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'morph', - providerId: PROVIDER_TYPES_INFO.morph.defaultProviderId, - id: 'morph-v0', - model: 'morph-v0', - }, { providerType: 'xai', providerId: PROVIDER_TYPES_INFO.xai.defaultProviderId, @@ -466,3 +391,9 @@ export const XAI_PRICES: Record = { 'grok-4-1-fast': { input: 0.2, output: 0.5 }, 'grok-4-1-fast-non-reasoning': { input: 0.2, output: 0.5 }, } + +export const DEEPSEEK_PRICES: Record = { + // Model version: DeepSeek-V3.2 + 'deepseek-chat': { input: 0.28, output: 0.42 }, + 'deepseek-reasoner': { input: 0.28, output: 0.42 }, +} diff --git a/src/core/llm/groq.ts b/src/core/llm/groq.ts deleted file mode 100644 index 768d7a38..00000000 --- a/src/core/llm/groq.ts +++ /dev/null @@ -1,65 +0,0 @@ -import OpenAI from 'openai' - -import { ChatModel } from '../../types/chat-model.types' -import { - LLMOptions, - LLMRequestNonStreaming, - LLMRequestStreaming, -} from '../../types/llm/request' -import { - LLMResponseNonStreaming, - LLMResponseStreaming, -} from '../../types/llm/response' -import { LLMProvider } from '../../types/provider.types' - -import { BaseLLMProvider } from './base' -import { OpenAIMessageAdapter } from './openaiMessageAdapter' - -export class GroqProvider extends BaseLLMProvider< - Extract -> { - private adapter: OpenAIMessageAdapter - private client: OpenAI - - constructor(provider: Extract) { - super(provider) - this.adapter = new OpenAIMessageAdapter() - this.client = new OpenAI({ - apiKey: provider.apiKey ?? '', - baseURL: provider.baseUrl - ? provider.baseUrl?.replace(/\/+$/, '') - : 'https://api.groq.com/openai/v1', - dangerouslyAllowBrowser: true, - }) - } - - async generateResponse( - model: ChatModel, - request: LLMRequestNonStreaming, - options?: LLMOptions, - ): Promise { - if (model.providerType !== 'groq') { - throw new Error('Model is not a Groq model') - } - - return this.adapter.generateResponse(this.client, request, options) - } - - async streamResponse( - model: ChatModel, - request: LLMRequestStreaming, - options?: LLMOptions, - ): Promise> { - if (model.providerType !== 'groq') { - throw new Error('Model is not a Groq model') - } - - return this.adapter.streamResponse(this.client, request, options) - } - - async getEmbedding(_model: string, _text: string): Promise { - throw new Error( - `Provider ${this.provider.id} does not support embeddings. Please use a different provider.`, - ) - } -} diff --git a/src/core/llm/manager.ts b/src/core/llm/manager.ts index 6aaeb7ad..2edf6a07 100644 --- a/src/core/llm/manager.ts +++ b/src/core/llm/manager.ts @@ -8,10 +8,8 @@ import { BaseLLMProvider } from './base' import { DeepSeekStudioProvider } from './deepseekStudioProvider' import { LLMModelNotFoundException } from './exception' import { GeminiProvider } from './gemini' -import { GroqProvider } from './groq' import { LmStudioProvider } from './lmStudioProvider' import { MistralProvider } from './mistralProvider' -import { MorphProvider } from './morphProvider' import { OllamaProvider } from './ollama' import { OpenAIAuthenticatedProvider } from './openai' import { OpenAICompatibleProvider } from './openaiCompatibleProvider' @@ -22,7 +20,6 @@ import { XaiProvider } from './xaiProvider' /* * OpenAI, OpenAI-compatible, and Anthropic providers include token usage statistics * in the final chunk of the stream (following OpenAI's behavior). - * Groq and Ollama currently do not support usage statistics for streaming responses. */ export function getProviderClient({ @@ -47,9 +44,6 @@ export function getProviderClient({ case 'gemini': { return new GeminiProvider(provider) } - case 'groq': { - return new GroqProvider(provider) - } case 'openrouter': { return new OpenRouterProvider(provider) } @@ -68,9 +62,6 @@ export function getProviderClient({ case 'mistral': { return new MistralProvider(provider) } - case 'morph': { - return new MorphProvider(provider) - } case 'xai': { return new XaiProvider(provider) } diff --git a/src/core/llm/morphProvider.ts b/src/core/llm/morphProvider.ts deleted file mode 100644 index ae2e258c..00000000 --- a/src/core/llm/morphProvider.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ChatModel } from '../../types/chat-model.types' -import { - LLMOptions, - LLMRequestNonStreaming, - LLMRequestStreaming, -} from '../../types/llm/request' -import { - LLMResponseNonStreaming, - LLMResponseStreaming, -} from '../../types/llm/response' -import { LLMProvider } from '../../types/provider.types' - -import { BaseLLMProvider } from './base' -import { NoStainlessOpenAI } from './NoStainlessOpenAI' -import { OpenAIMessageAdapter } from './openaiMessageAdapter' - -export class MorphProvider extends BaseLLMProvider< - Extract -> { - private adapter: OpenAIMessageAdapter - private client: NoStainlessOpenAI - - constructor(provider: Extract) { - super(provider) - this.adapter = new OpenAIMessageAdapter() - this.client = new NoStainlessOpenAI({ - baseURL: `${provider.baseUrl ? provider.baseUrl.replace(/\/+$/, '') : 'https://api.morphllm.com'}/v1`, - apiKey: provider.apiKey ?? '', - dangerouslyAllowBrowser: true, - }) - } - - async generateResponse( - model: ChatModel, - request: LLMRequestNonStreaming, - options?: LLMOptions, - ): Promise { - if (model.providerType !== 'morph') { - throw new Error('Model is not an morph model') - } - - return this.adapter.generateResponse( - this.client, - { - ...request, - prediction: undefined, // morph doesn't support prediction - }, - options, - ) - } - - async streamResponse( - model: ChatModel, - request: LLMRequestStreaming, - options?: LLMOptions, - ): Promise> { - if (model.providerType !== 'morph') { - throw new Error('Model is not an morph model') - } - - return this.adapter.streamResponse( - this.client, - { - ...request, - prediction: undefined, // morph doesn't support prediction - }, - options, - ) - } - - async getEmbedding(_model: string, _text: string): Promise { - throw new Error( - `Provider ${this.provider.id} does not support embeddings. Please use a different provider.`, - ) - } -} diff --git a/src/settings/schema/migrations/13_to_14.test.ts b/src/settings/schema/migrations/13_to_14.test.ts index b387c639..4e97c72c 100644 --- a/src/settings/schema/migrations/13_to_14.test.ts +++ b/src/settings/schema/migrations/13_to_14.test.ts @@ -9,15 +9,196 @@ describe('Migration from v13 to v14', () => { expect(result.version).toBe(14) }) + it('should add xai provider and drop unused groq/morph providers', () => { + const oldSettings = { + version: 13, + providers: [ + { type: 'openai', id: 'openai', apiKey: 'openai-key' }, + { type: 'anthropic', id: 'anthropic', apiKey: 'anthropic-key' }, + { type: 'gemini', id: 'gemini', apiKey: 'gemini-key' }, + { type: 'groq', id: 'groq', apiKey: 'groq-key' }, + { type: 'deepseek', id: 'deepseek', apiKey: 'deepseek-key' }, + { type: 'morph', id: 'morph', apiKey: 'morph-key' }, + ], + chatModels: [], // No models using groq or morph + } + const result = migrateFrom13To14(oldSettings) + expect(result.version).toBe(14) + + const providers = result.providers as { + type: string + id: string + }[] + + // xai should be added + expect(providers.find((p) => p.type === 'xai')).toBeDefined() + + // groq and morph should be dropped (no models use them) + expect(providers.find((p) => p.id === 'groq')).toBeUndefined() + expect(providers.find((p) => p.id === 'morph')).toBeUndefined() + }) + + it('should convert groq/morph to openai-compatible when models use them', () => { + const oldSettings = { + version: 13, + providers: [ + { type: 'groq', id: 'groq', apiKey: 'groq-key' }, + { type: 'morph', id: 'morph', apiKey: 'morph-key' }, + ], + chatModels: [ + { + id: 'llama-groq', + providerType: 'groq', + providerId: 'groq', + model: 'llama-3.3-70b', + }, + { + id: 'morph-v0', + providerType: 'morph', + providerId: 'morph', + model: 'morph-v0', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const providers = result.providers as { + type: string + id: string + baseUrl?: string + apiKey?: string + }[] + + // groq and morph should be converted to openai-compatible + const groqProvider = providers.find((p) => p.id === 'groq') + expect(groqProvider?.type).toBe('openai-compatible') + expect(groqProvider?.baseUrl).toBe('https://api.groq.com/openai/v1') + expect(groqProvider?.apiKey).toBe('groq-key') + + const morphProvider = providers.find((p) => p.id === 'morph') + expect(morphProvider?.type).toBe('openai-compatible') + expect(morphProvider?.baseUrl).toBe('https://api.morphllm.com/v1') + expect(morphProvider?.apiKey).toBe('morph-key') + + // Chat models should also be converted + const chatModels = result.chatModels as { + id: string + providerType: string + }[] + expect(chatModels.find((m) => m.id === 'llama-groq')?.providerType).toBe( + 'openai-compatible', + ) + expect(chatModels.find((m) => m.id === 'morph-v0')?.providerType).toBe( + 'openai-compatible', + ) + }) + + it('should preserve custom baseUrl for groq/morph providers', () => { + const oldSettings = { + version: 13, + providers: [ + { + type: 'groq', + id: 'groq', + apiKey: 'groq-key', + baseUrl: 'https://custom-groq.example.com/v1', + }, + { + type: 'morph', + id: 'morph', + apiKey: 'morph-key', + baseUrl: 'https://custom-morph.example.com/v1', + }, + ], + chatModels: [ + { + id: 'groq-model', + providerType: 'groq', + providerId: 'groq', + model: 'x', + }, + { + id: 'morph-model', + providerType: 'morph', + providerId: 'morph', + model: 'y', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const providers = result.providers as { + type: string + id: string + baseUrl?: string + }[] + + const groqProvider = providers.find((p) => p.id === 'groq') + expect(groqProvider?.baseUrl).toBe('https://custom-groq.example.com/v1') + + const morphProvider = providers.find((p) => p.id === 'morph') + expect(morphProvider?.baseUrl).toBe('https://custom-morph.example.com/v1') + }) + + it('should preserve existing API keys after migration', () => { + const oldSettings = { + version: 13, + providers: [ + { type: 'openai', id: 'openai', apiKey: 'sk-openai-secret-key' }, + { type: 'anthropic', id: 'anthropic', apiKey: 'sk-ant-secret-key' }, + { type: 'gemini', id: 'gemini', apiKey: 'gemini-api-key-123' }, + { type: 'deepseek', id: 'deepseek', apiKey: 'deepseek-key-456' }, + { type: 'mistral', id: 'mistral', apiKey: 'mistral-key-789' }, + { type: 'perplexity', id: 'perplexity', apiKey: 'pplx-key' }, + { type: 'openrouter', id: 'openrouter', apiKey: 'openrouter-key' }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const providers = result.providers as { + type: string + id: string + apiKey?: string + }[] + + // Verify all API keys are preserved + expect(providers.find((p) => p.type === 'openai')?.apiKey).toBe( + 'sk-openai-secret-key', + ) + expect(providers.find((p) => p.type === 'anthropic')?.apiKey).toBe( + 'sk-ant-secret-key', + ) + expect(providers.find((p) => p.type === 'gemini')?.apiKey).toBe( + 'gemini-api-key-123', + ) + expect(providers.find((p) => p.type === 'deepseek')?.apiKey).toBe( + 'deepseek-key-456', + ) + expect(providers.find((p) => p.type === 'mistral')?.apiKey).toBe( + 'mistral-key-789', + ) + expect(providers.find((p) => p.type === 'perplexity')?.apiKey).toBe( + 'pplx-key', + ) + expect(providers.find((p) => p.type === 'openrouter')?.apiKey).toBe( + 'openrouter-key', + ) + + // Verify new xai provider is added (without API key since it's new) + const xaiProvider = providers.find((p) => p.type === 'xai') + expect(xaiProvider).toBeDefined() + expect(xaiProvider?.apiKey).toBeUndefined() + }) + it('should merge existing chat models with new default models', () => { const oldSettings = { version: 13, chatModels: [ { - id: 'gpt-5-mini', - providerType: 'openai', - providerId: 'openai', - model: 'gpt-5-mini', + id: 'claude-sonnet-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-sonnet-4-5', enable: false, }, { @@ -32,7 +213,7 @@ describe('Migration from v13 to v14', () => { expect(result.chatModels).toEqual([ ...DEFAULT_CHAT_MODELS_V14.map((model) => - model.id === 'gpt-5-mini' + model.id === 'claude-sonnet-4.5' ? { ...model, enable: false, @@ -48,33 +229,33 @@ describe('Migration from v13 to v14', () => { ]) }) - it('should add new GPT-5.2 model', () => { + it('should add new Claude Opus 4.5 model', () => { const oldSettings = { version: 13, chatModels: [ { - id: 'gpt-5-mini', - providerType: 'openai', - providerId: 'openai', - model: 'gpt-5-mini', + id: 'claude-sonnet-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-sonnet-4-5', }, ], } const result = migrateFrom13To14(oldSettings) const chatModels = result.chatModels as { id: string }[] - const gpt52 = chatModels.find((m) => m.id === 'gpt-5.2') + const opus45 = chatModels.find((m) => m.id === 'claude-opus-4.5') - expect(gpt52).toBeDefined() - expect(gpt52).toEqual({ - id: 'gpt-5.2', - providerType: 'openai', - providerId: 'openai', - model: 'gpt-5.2', + expect(opus45).toBeDefined() + expect(opus45).toEqual({ + id: 'claude-opus-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-opus-4-5', }) }) - it('should add new gpt-4.1-mini model', () => { + it('should add new GPT-5.2 and GPT-4.1-mini models', () => { const oldSettings = { version: 13, chatModels: [ @@ -89,10 +270,19 @@ describe('Migration from v13 to v14', () => { const result = migrateFrom13To14(oldSettings) const chatModels = result.chatModels as { id: string }[] - const gpt41mini = chatModels.find((m) => m.id === 'gpt-4.1-mini') + const gpt52 = chatModels.find((m) => m.id === 'gpt-5.2') + const gpt41Mini = chatModels.find((m) => m.id === 'gpt-4.1-mini') - expect(gpt41mini).toBeDefined() - expect(gpt41mini).toEqual({ + expect(gpt52).toBeDefined() + expect(gpt52).toEqual({ + id: 'gpt-5.2', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.2', + }) + + expect(gpt41Mini).toBeDefined() + expect(gpt41Mini).toEqual({ id: 'gpt-4.1-mini', providerType: 'openai', providerId: 'openai', @@ -100,7 +290,67 @@ describe('Migration from v13 to v14', () => { }) }) - it('should preserve gpt-5.1 as custom model when migrating', () => { + it('should add new Gemini 3 models', () => { + const oldSettings = { + version: 13, + chatModels: [], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gemini3Pro = chatModels.find((m) => m.id === 'gemini-3-pro-preview') + const gemini3Flash = chatModels.find( + (m) => m.id === 'gemini-3-flash-preview', + ) + + expect(gemini3Pro).toBeDefined() + expect(gemini3Pro).toEqual({ + id: 'gemini-3-pro-preview', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-3-pro-preview', + }) + + expect(gemini3Flash).toBeDefined() + expect(gemini3Flash).toEqual({ + id: 'gemini-3-flash-preview', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-3-flash-preview', + }) + }) + + it('should add new Grok models from xai provider', () => { + const oldSettings = { + version: 13, + chatModels: [], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const grokFast = chatModels.find((m) => m.id === 'grok-4-1-fast') + const grokNonReasoning = chatModels.find( + (m) => m.id === 'grok-4-1-fast-non-reasoning', + ) + + expect(grokFast).toBeDefined() + expect(grokFast).toEqual({ + id: 'grok-4-1-fast', + providerType: 'xai', + providerId: 'xai', + model: 'grok-4-1-fast', + }) + + expect(grokNonReasoning).toBeDefined() + expect(grokNonReasoning).toEqual({ + id: 'grok-4-1-fast-non-reasoning', + providerType: 'xai', + providerId: 'xai', + model: 'grok-4-1-fast-non-reasoning', + }) + }) + + it('should preserve removed models as custom models', () => { const oldSettings = { version: 13, chatModels: [ @@ -112,20 +362,35 @@ describe('Migration from v13 to v14', () => { enable: true, }, { - id: 'gpt-5-mini', + id: 'gpt-4o', providerType: 'openai', providerId: 'openai', - model: 'gpt-5-mini', + model: 'gpt-4o', + }, + { + id: 'morph-v0', + providerType: 'morph', + providerId: 'morph', + model: 'morph-v0', + }, + { + id: 'sonar', + providerType: 'perplexity', + providerId: 'perplexity', + model: 'sonar', }, ], } const result = migrateFrom13To14(oldSettings) const chatModels = result.chatModels as { id: string }[] + + // These models should be preserved as custom models since they are not in DEFAULT_CHAT_MODELS_V14 const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') - const gpt52 = chatModels.find((m) => m.id === 'gpt-5.2') + const gpt4o = chatModels.find((m) => m.id === 'gpt-4o') + const morphV0 = chatModels.find((m) => m.id === 'morph-v0') + const sonar = chatModels.find((m) => m.id === 'sonar') - // gpt-5.1 should be preserved as custom model expect(gpt51).toBeDefined() expect(gpt51).toEqual({ id: 'gpt-5.1', @@ -135,157 +400,636 @@ describe('Migration from v13 to v14', () => { enable: true, }) - // gpt-5.2 should be added as new default - expect(gpt52).toBeDefined() - expect(gpt52).toEqual({ - id: 'gpt-5.2', + expect(gpt4o).toBeDefined() + expect(gpt4o).toEqual({ + id: 'gpt-4o', providerType: 'openai', providerId: 'openai', - model: 'gpt-5.2', + model: 'gpt-4o', + }) + + expect(morphV0).toBeDefined() + expect(morphV0).toEqual({ + id: 'morph-v0', + providerType: 'openai-compatible', + providerId: 'morph', + model: 'morph-v0', + }) + + expect(sonar).toBeDefined() + expect(sonar).toEqual({ + id: 'sonar', + providerType: 'perplexity', + providerId: 'perplexity', + model: 'sonar', }) }) - it('should add claude-opus-4.5 as new default model', () => { + it('should preserve o4-mini with reasoning settings', () => { const oldSettings = { version: 13, chatModels: [ { - id: 'claude-sonnet-4.5', - providerType: 'anthropic', - providerId: 'anthropic', - model: 'claude-sonnet-4-5', + id: 'o4-mini', + providerType: 'openai', + providerId: 'openai', + model: 'o4-mini', + reasoning: { + enabled: true, + reasoning_effort: 'high', + }, }, ], } const result = migrateFrom13To14(oldSettings) - const chatModels = result.chatModels as { id: string }[] - const opus45 = chatModels.find((m) => m.id === 'claude-opus-4.5') + const chatModels = result.chatModels as { + id: string + reasoning?: { enabled: boolean; reasoning_effort?: string } + }[] + const o4Mini = chatModels.find((m) => m.id === 'o4-mini') - // claude-opus-4.5 should be added as new default - expect(opus45).toBeDefined() - expect(opus45).toEqual({ - id: 'claude-opus-4.5', - providerType: 'anthropic', - providerId: 'anthropic', - model: 'claude-opus-4-5', + expect(o4Mini).toBeDefined() + // Note: Object.assign does shallow merge, so reasoning settings from default override user settings + expect(o4Mini?.reasoning).toEqual({ + enabled: true, + reasoning_effort: 'medium', }) }) +}) - it('should preserve claude-opus-4.1 as custom model when migrating', () => { - const oldSettings = { - version: 13, - chatModels: [ +describe('Test with real data', () => { + const oldSettings = { + version: 13, + providers: [ + { + type: 'openai', + id: 'openai', + apiKey: 'test-openai-api-key', + }, + { + type: 'anthropic', + id: 'anthropic', + apiKey: 'test-anthropic-api-key', + }, + { + type: 'gemini', + id: 'gemini', + apiKey: 'test-gemini-api-key', + }, + { + type: 'deepseek', + id: 'deepseek', + apiKey: 'test-deepseek-api-key', + }, + { + type: 'perplexity', + id: 'perplexity', + apiKey: 'test-perplexity-api-key', + }, + { + type: 'groq', + id: 'groq', + apiKey: 'test-groq-api-key', + }, + { + type: 'mistral', + id: 'mistral', + }, + { + type: 'openrouter', + id: 'openrouter', + baseUrl: '', + apiKey: 'test-openrouter-api-key', + }, + { + type: 'ollama', + id: 'ollama', + baseUrl: 'ollama-url-test', + }, + { + type: 'lm-studio', + id: 'lm-studio', + }, + { + type: 'morph', + id: 'morph', + }, + { + type: 'openai-compatible', + id: 'siliconflow', + baseUrl: 'siliconflow-test', + apiKey: '', + }, + ], + chatModels: [ + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-opus-4.1', + model: 'claude-opus-4-1', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-sonnet-4.5', + model: 'claude-sonnet-4-5', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-haiku-4.5', + model: 'claude-haiku-4-5', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5.1', + model: 'gpt-5.1', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5', + model: 'gpt-5', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-mini', + model: 'gpt-5-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-nano', + model: 'gpt-5-nano', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1', + model: 'gpt-4.1', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1-mini', + model: 'gpt-4.1-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1-nano', + model: 'gpt-4.1-nano', + enable: false, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o', + model: 'gpt-4o', + enable: false, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o-mini', + model: 'gpt-4o-mini', + enable: false, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o4-mini', + model: 'o4-mini', + enable: false, + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o3', + model: 'o3', + enable: false, + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-pro', + model: 'gemini-2.5-pro', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash', + model: 'gemini-2.5-flash', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash-lite', + model: 'gemini-2.5-flash-lite', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash', + model: 'gemini-2.0-flash', + enable: false, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash-lite', + model: 'gemini-2.0-flash-lite', + enable: false, + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-chat', + model: 'deepseek-chat', + enable: false, + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-reasoner', + model: 'deepseek-reasoner', + enable: false, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar', + model: 'sonar', + enable: false, + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-pro', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-deep-research', + model: 'sonar-deep-research', + enable: false, + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning', + model: 'sonar', + enable: false, + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning-pro', + model: 'sonar', + enable: false, + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'morph', + providerId: 'morph', + id: 'morph-v0', + model: 'morph-v0', + enable: false, + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-sonnet-4.0', + model: 'claude-sonnet-4-0', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-3.7-sonnet', + model: 'claude-3-7-sonnet-latest', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-3.5-sonnet', + model: 'claude-3-5-sonnet-latest', + enable: false, + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-3.5-haiku', + model: 'claude-3-5-haiku-latest', + enable: false, + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-3.7-sonnet-thinking', + model: 'claude-3-7-sonnet-latest', + thinking: { + enabled: true, + budget_tokens: 8192, + }, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash-thinking', + model: 'gemini-2.0-flash-thinking-exp', + enable: false, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-1.5-pro', + model: 'gemini-1.5-pro', + enable: false, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-1.5-flash', + model: 'gemini-1.5-flash', + enable: false, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-exp-1206', + model: 'gemini-exp-1206', + enable: false, + }, + { + providerType: 'openrouter', + providerId: 'openrouter', + id: 'llama-3.3-70b-instruct', + model: 'meta-llama/llama-3.3-70b-instruct', + }, + { + providerType: 'openai-compatible', + providerId: 'mistral', + id: 'mistral-small-latest', + model: 'mistral-small-latest', + promptLevel: 1, + }, + ], + embeddingModels: [ + { + providerType: 'openai', + providerId: 'openai', + id: 'openai/text-embedding-3-small', + model: 'text-embedding-3-small', + dimension: 1536, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'openai/text-embedding-3-large', + model: 'text-embedding-3-large', + dimension: 3072, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini/text-embedding-004', + model: 'text-embedding-004', + dimension: 768, + }, + { + providerType: 'ollama', + providerId: 'ollama', + id: 'ollama/nomic-embed-text', + model: 'nomic-embed-text', + dimension: 768, + }, + { + providerType: 'ollama', + providerId: 'ollama', + id: 'ollama/mxbai-embed-large', + model: 'mxbai-embed-large', + dimension: 1024, + }, + { + providerType: 'ollama', + providerId: 'ollama', + id: 'ollama/bge-m3', + model: 'bge-m3', + dimension: 1024, + }, + ], + chatModelId: 'gpt-4.1', + applyModelId: 'gpt-4.1-mini', + embeddingModelId: 'openai/text-embedding-3-small', + systemPrompt: '', + ragOptions: { + chunkSize: 1000, + thresholdTokens: 8192, + minSimilarity: 0, + limit: 10, + excludePatterns: [], + includePatterns: [], + }, + mcp: { + servers: [ { - id: 'claude-opus-4.1', - providerType: 'anthropic', - providerId: 'anthropic', - model: 'claude-opus-4-1', - enable: true, + id: 'markitdown', + parameters: { + command: 'uvx', + args: ['markitdown-mcp'], + }, + enabled: true, + toolOptions: {}, }, ], - } + }, + chatOptions: { + includeCurrentFileContent: true, + enableTools: false, + maxAutoIterations: 1, + }, + } + + it('should migrate real data without errors', () => { const result = migrateFrom13To14(oldSettings) + expect(result.version).toBe(14) + }) - const chatModels = result.chatModels as { id: string }[] - const opus41 = chatModels.find((m) => m.id === 'claude-opus-4.1') - const opus45 = chatModels.find((m) => m.id === 'claude-opus-4.5') + it('should preserve all API keys from real data', () => { + const result = migrateFrom13To14(oldSettings) + const providers = result.providers as { + type: string + id: string + apiKey?: string + baseUrl?: string + }[] - // claude-opus-4.1 should be preserved as custom model - expect(opus41).toBeDefined() - expect(opus41).toEqual({ - id: 'claude-opus-4.1', - providerType: 'anthropic', - providerId: 'anthropic', - model: 'claude-opus-4-1', - enable: true, - }) + expect(providers.find((p) => p.type === 'openai')?.apiKey).toBe( + 'test-openai-api-key', + ) + expect(providers.find((p) => p.type === 'anthropic')?.apiKey).toBe( + 'test-anthropic-api-key', + ) + expect(providers.find((p) => p.type === 'gemini')?.apiKey).toBe( + 'test-gemini-api-key', + ) + expect(providers.find((p) => p.type === 'deepseek')?.apiKey).toBe( + 'test-deepseek-api-key', + ) + expect(providers.find((p) => p.type === 'perplexity')?.apiKey).toBe( + 'test-perplexity-api-key', + ) + expect(providers.find((p) => p.type === 'openrouter')?.apiKey).toBe( + 'test-openrouter-api-key', + ) + }) - // claude-opus-4.5 should be added as new default - expect(opus45).toBeDefined() - expect(opus45).toEqual({ - id: 'claude-opus-4.5', - providerType: 'anthropic', - providerId: 'anthropic', - model: 'claude-opus-4-5', - }) + it('should preserve custom provider configurations from real data', () => { + const result = migrateFrom13To14(oldSettings) + const providers = result.providers as { + type: string + id: string + apiKey?: string + baseUrl?: string + }[] + + // groq should be dropped (no models use it) + expect(providers.find((p) => p.id === 'groq')).toBeUndefined() + + // morph should be converted to openai-compatible (morph-v0 model uses it) + const morphProvider = providers.find((p) => p.id === 'morph') + expect(morphProvider?.type).toBe('openai-compatible') + expect(morphProvider?.baseUrl).toBe('https://api.morphllm.com/v1') + + // ollama with custom baseUrl should be preserved + const ollama = providers.find((p) => p.type === 'ollama') + expect(ollama?.baseUrl).toBe('ollama-url-test') + + // openai-compatible provider should be preserved + const siliconflow = providers.find((p) => p.id === 'siliconflow') + expect(siliconflow).toBeDefined() + expect(siliconflow?.baseUrl).toBe('siliconflow-test') }) - it('should add gemini-3-pro-preview and gemini-3-flash-preview as new default models', () => { - const oldSettings = { - version: 13, - chatModels: [ - { - id: 'gpt-5-mini', - providerType: 'openai', - providerId: 'openai', - model: 'gpt-5-mini', - }, - ], - } + it('should preserve user model settings from real data', () => { const result = migrateFrom13To14(oldSettings) + const chatModels = result.chatModels as { + id: string + enable?: boolean + thinking?: { enabled: boolean; budget_tokens: number } + web_search_options?: { search_context_size: string } + promptLevel?: number + }[] - const chatModels = result.chatModels as { id: string }[] - const gemini3Pro = chatModels.find((m) => m.id === 'gemini-3-pro-preview') - const gemini3Flash = chatModels.find( - (m) => m.id === 'gemini-3-flash-preview', - ) + // Check that user's disabled models remain disabled + const gpt4o = chatModels.find((m) => m.id === 'gpt-4o') + expect(gpt4o?.enable).toBe(false) - // gemini-3-pro-preview should be added as new default - expect(gemini3Pro).toBeDefined() - expect(gemini3Pro).toEqual({ - id: 'gemini-3-pro-preview', - providerType: 'gemini', - providerId: 'gemini', - model: 'gemini-3-pro-preview', + // Check that models with thinking settings are preserved + const claude37Thinking = chatModels.find( + (m) => m.id === 'claude-3.7-sonnet-thinking', + ) + expect(claude37Thinking?.thinking).toEqual({ + enabled: true, + budget_tokens: 8192, }) - // gemini-3-flash-preview should be added as new default - expect(gemini3Flash).toBeDefined() - expect(gemini3Flash).toEqual({ - id: 'gemini-3-flash-preview', - providerType: 'gemini', - providerId: 'gemini', - model: 'gemini-3-flash-preview', - }) + // Check that custom model with promptLevel is preserved + const mistralSmall = chatModels.find((m) => m.id === 'mistral-small-latest') + expect(mistralSmall?.promptLevel).toBe(1) }) - it('should preserve gemini-2.5-pro as custom model when migrating', () => { - const oldSettings = { - version: 13, - chatModels: [ - { - id: 'gemini-2.5-pro', - providerType: 'gemini', - providerId: 'gemini', - model: 'gemini-2.5-pro', - enable: true, - }, - ], - } + it('should add xai provider for real data', () => { const result = migrateFrom13To14(oldSettings) + const providers = result.providers as { type: string; id: string }[] + + const xai = providers.find((p) => p.type === 'xai') + expect(xai).toBeDefined() + }) + it('should add new default models to real data', () => { + const result = migrateFrom13To14(oldSettings) const chatModels = result.chatModels as { id: string }[] - const gemini25Pro = chatModels.find((m) => m.id === 'gemini-2.5-pro') - const gemini3Pro = chatModels.find((m) => m.id === 'gemini-3-pro-preview') - // gemini-2.5-pro should be preserved as custom model - expect(gemini25Pro).toBeDefined() - expect(gemini25Pro).toEqual({ - id: 'gemini-2.5-pro', - providerType: 'gemini', - providerId: 'gemini', - model: 'gemini-2.5-pro', - enable: true, - }) + // New models should be added + expect(chatModels.find((m) => m.id === 'gpt-5.2')).toBeDefined() + expect(chatModels.find((m) => m.id === 'claude-opus-4.5')).toBeDefined() + expect( + chatModels.find((m) => m.id === 'gemini-3-pro-preview'), + ).toBeDefined() + expect( + chatModels.find((m) => m.id === 'gemini-3-flash-preview'), + ).toBeDefined() + expect(chatModels.find((m) => m.id === 'grok-4-1-fast')).toBeDefined() + expect( + chatModels.find((m) => m.id === 'grok-4-1-fast-non-reasoning'), + ).toBeDefined() + }) - // gemini-3-pro-preview should be added as new default - expect(gemini3Pro).toBeDefined() - expect(gemini3Pro).toEqual({ - id: 'gemini-3-pro-preview', - providerType: 'gemini', - providerId: 'gemini', - model: 'gemini-3-pro-preview', + it('should preserve removed models as custom models from real data', () => { + const result = migrateFrom13To14(oldSettings) + const chatModels = result.chatModels as { id: string }[] + + // Models removed from defaults should still be preserved + expect(chatModels.find((m) => m.id === 'gpt-5.1')).toBeDefined() + expect(chatModels.find((m) => m.id === 'gpt-5-nano')).toBeDefined() + expect(chatModels.find((m) => m.id === 'claude-opus-4.1')).toBeDefined() + expect(chatModels.find((m) => m.id === 'gemini-2.5-pro')).toBeDefined() + expect(chatModels.find((m) => m.id === 'morph-v0')).toBeDefined() + expect(chatModels.find((m) => m.id === 'sonar')).toBeDefined() + }) + + it('should preserve other settings from real data', () => { + const result = migrateFrom13To14(oldSettings) + + expect(result.chatModelId).toBe('gpt-4.1') + expect(result.applyModelId).toBe('gpt-4.1-mini') + expect(result.embeddingModelId).toBe('openai/text-embedding-3-small') + expect(result.systemPrompt).toBe('') + expect(result.ragOptions).toEqual({ + chunkSize: 1000, + thresholdTokens: 8192, + minSimilarity: 0, + limit: 10, + excludePatterns: [], + includePatterns: [], + }) + expect(result.mcp).toBeDefined() + expect(result.chatOptions).toEqual({ + includeCurrentFileContent: true, + enableTools: false, + maxAutoIterations: 1, }) }) + + it('should preserve embedding models from real data', () => { + const result = migrateFrom13To14(oldSettings) + expect(result.embeddingModels).toEqual(oldSettings.embeddingModels) + }) }) diff --git a/src/settings/schema/migrations/13_to_14.ts b/src/settings/schema/migrations/13_to_14.ts index 8ab5d55b..10799f07 100644 --- a/src/settings/schema/migrations/13_to_14.ts +++ b/src/settings/schema/migrations/13_to_14.ts @@ -5,6 +5,7 @@ import { getMigratedChatModels, getMigratedProviders } from './migrationUtils' /** * Migration from version 13 to version 14 * - Add xai provider + * - Convert groq and morph providers to openai-compatible * - Add following models: * - gpt-5.2 * - gpt-4.1-mini @@ -25,11 +26,59 @@ import { getMigratedChatModels, getMigratedProviders } from './migrationUtils' * - gemini-2.5-flash-lite * - gemini-2.0-flash * - gemini-2.0-flash-lite + * - morph-v0 + * - sonar, sonar-pro, sonar-deep-research, sonar-reasoning, sonar-reasoning-pro */ + +const LEGACY_PROVIDERS: Record = { + groq: 'https://api.groq.com/openai/v1', + morph: 'https://api.morphllm.com/v1', +} + +type ProviderRecord = Record & { type: string; id: string } +type ChatModelRecord = Record & { + providerType: string + providerId: string +} + +function migrateLegacyProviders(data: Record) { + const providers = (data.providers ?? []) as ProviderRecord[] + const chatModels = (data.chatModels ?? []) as ChatModelRecord[] + + // Get provider IDs that have models using them + const usedProviderIds = new Set( + chatModels + .filter((m) => m.providerType in LEGACY_PROVIDERS) + .map((m) => m.providerId), + ) + + // Convert used legacy providers, drop unused ones + data.providers = providers.flatMap((p) => { + if (!(p.type in LEGACY_PROVIDERS)) return [p] + if (!usedProviderIds.has(p.id)) return [] + return [ + { + ...p, + type: 'openai-compatible', + baseUrl: p.baseUrl || LEGACY_PROVIDERS[p.type], + }, + ] + }) + + // Convert legacy chat models + data.chatModels = chatModels.map((m) => + m.providerType in LEGACY_PROVIDERS + ? { ...m, providerType: 'openai-compatible' } + : m, + ) +} + export const migrateFrom13To14: SettingMigration['migrate'] = (data) => { const newData = { ...data } newData.version = 14 + migrateLegacyProviders(newData) + newData.providers = getMigratedProviders(newData, DEFAULT_PROVIDERS_V14) newData.chatModels = getMigratedChatModels(newData, DEFAULT_CHAT_MODELS_V14) @@ -40,15 +89,13 @@ const DEFAULT_PROVIDERS_V14 = [ { type: 'openai', id: 'openai' }, { type: 'anthropic', id: 'anthropic' }, { type: 'gemini', id: 'gemini' }, + { type: 'xai', id: 'xai' }, { type: 'deepseek', id: 'deepseek' }, - { type: 'perplexity', id: 'perplexity' }, - { type: 'groq', id: 'groq' }, { type: 'mistral', id: 'mistral' }, + { type: 'perplexity', id: 'perplexity' }, { type: 'openrouter', id: 'openrouter' }, { type: 'ollama', id: 'ollama' }, { type: 'lm-studio', id: 'lm-studio' }, - { type: 'morph', id: 'morph' }, - { type: 'xai', id: 'xai' }, ] as const type DefaultChatModelsV14 = { @@ -64,9 +111,6 @@ type DefaultChatModelsV14 = { enabled: boolean budget_tokens: number } - web_search_options?: { - search_context_size?: string - } enable?: boolean }[] @@ -141,57 +185,6 @@ export const DEFAULT_CHAT_MODELS_V14: DefaultChatModelsV14 = [ id: 'deepseek-reasoner', model: 'deepseek-reasoner', }, - { - providerType: 'perplexity', - providerId: 'perplexity', - id: 'sonar', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: 'perplexity', - id: 'sonar-pro', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: 'perplexity', - id: 'sonar-deep-research', - model: 'sonar-deep-research', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: 'perplexity', - id: 'sonar-reasoning', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: 'perplexity', - id: 'sonar-reasoning-pro', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'morph', - providerId: 'morph', - id: 'morph-v0', - model: 'morph-v0', - }, { providerType: 'xai', providerId: 'xai', diff --git a/src/settings/schema/migrations/1_to_2.ts b/src/settings/schema/migrations/1_to_2.ts index 5844dae3..531bcf0d 100644 --- a/src/settings/schema/migrations/1_to_2.ts +++ b/src/settings/schema/migrations/1_to_2.ts @@ -1,10 +1,15 @@ import { z } from 'zod' -import { ChatModel } from '../../../types/chat-model.types' -import { EmbeddingModel } from '../../../types/embedding-model.types' -import { LLMProvider } from '../../../types/provider.types' import { SettingMigration } from '../setting.types' +// Migration-local types (frozen at v2 state to avoid breaking changes when main types change) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type V2LLMProvider = any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type V2ChatModel = any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type V2EmbeddingModel = any + type NativeLLMModel = { provider: 'openai' | 'anthropic' | 'gemini' | 'groq' model: string @@ -427,7 +432,7 @@ export const V2_PROVIDER_TYPES_INFO = { }, } as const -export const V2_DEFAULT_PROVIDERS: readonly LLMProvider[] = [ +export const V2_DEFAULT_PROVIDERS: readonly V2LLMProvider[] = [ { type: 'openai', id: V2_PROVIDER_TYPES_INFO.openai.defaultProviderId, @@ -454,7 +459,7 @@ export const V2_DEFAULT_PROVIDERS: readonly LLMProvider[] = [ }, ] -export const V2_DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ +export const V2_DEFAULT_CHAT_MODELS: readonly V2ChatModel[] = [ { providerType: 'anthropic', providerId: V2_PROVIDER_TYPES_INFO.anthropic.defaultProviderId, @@ -508,7 +513,6 @@ export const V2_DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ providerId: V2_PROVIDER_TYPES_INFO.openai.defaultProviderId, id: 'o1', model: 'o1', - // @ts-expect-error: streamingDisabled is deprecated streamingDisabled: true, // currently, o1 API doesn't support streaming }, { @@ -531,7 +535,7 @@ export const V2_DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ }, ] -export const V2_DEFAULT_EMBEDDING_MODELS: readonly EmbeddingModel[] = [ +export const V2_DEFAULT_EMBEDDING_MODELS: readonly V2EmbeddingModel[] = [ { providerType: 'openai', providerId: V2_PROVIDER_TYPES_INFO.openai.defaultProviderId, @@ -579,8 +583,8 @@ export const V2_DEFAULT_EMBEDDING_MODELS: readonly EmbeddingModel[] = [ export const migrateFrom1To2: SettingMigration['migrate'] = ( data: SmartComposerSettingsV1, ) => { - const providers: LLMProvider[] = [...V2_DEFAULT_PROVIDERS] - const chatModels: ChatModel[] = [...V2_DEFAULT_CHAT_MODELS] + const providers: V2LLMProvider[] = [...V2_DEFAULT_PROVIDERS] + const chatModels: V2ChatModel[] = [...V2_DEFAULT_CHAT_MODELS] // Map old model IDs to new model IDs const MODEL_ID_MAP: Record = { diff --git a/src/settings/schema/migrations/2_to_3.ts b/src/settings/schema/migrations/2_to_3.ts index 66a2d14d..b428d899 100644 --- a/src/settings/schema/migrations/2_to_3.ts +++ b/src/settings/schema/migrations/2_to_3.ts @@ -1,39 +1,45 @@ -import { PROVIDER_TYPES_INFO } from '../../../constants' -import { ChatModel } from '../../../types/chat-model.types' -import { LLMProvider } from '../../../types/provider.types' import { SettingMigration } from '../setting.types' -export const NEW_DEFAULT_PROVIDERS: LLMProvider[] = [ +// Provider IDs at version 3 (hardcoded to avoid dependency on current constants) +const V3_PROVIDER_IDS = { + 'lm-studio': 'lm-studio', + deepseek: 'deepseek', + morph: 'morph', +} as const + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const NEW_DEFAULT_PROVIDERS: any[] = [ { type: 'lm-studio', - id: PROVIDER_TYPES_INFO['lm-studio'].defaultProviderId, + id: V3_PROVIDER_IDS['lm-studio'], }, { type: 'deepseek', - id: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, + id: V3_PROVIDER_IDS.deepseek, }, { type: 'morph', - id: PROVIDER_TYPES_INFO.morph.defaultProviderId, + id: V3_PROVIDER_IDS.morph, }, ] -export const NEW_DEFAULT_CHAT_MODELS: ChatModel[] = [ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const NEW_DEFAULT_CHAT_MODELS: any[] = [ { providerType: 'deepseek', - providerId: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, + providerId: V3_PROVIDER_IDS.deepseek, id: 'deepseek-chat', model: 'deepseek-chat', }, { providerType: 'deepseek', - providerId: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, + providerId: V3_PROVIDER_IDS.deepseek, id: 'deepseek-reasoner', model: 'deepseek-reasoner', }, { providerType: 'morph', - providerId: PROVIDER_TYPES_INFO.morph.defaultProviderId, + providerId: V3_PROVIDER_IDS.morph, id: 'morph-v0', model: 'morph-v0', }, diff --git a/src/types/chat-model.types.ts b/src/types/chat-model.types.ts index a549caf9..4e05b57a 100644 --- a/src/types/chat-model.types.ts +++ b/src/types/chat-model.types.ts @@ -64,23 +64,15 @@ export const chatModelSchema = z.discriminatedUnion('providerType', [ .optional(), }), z.object({ - providerType: z.literal('groq'), - ...baseChatModelSchema.shape, - }), - z.object({ - providerType: z.literal('openrouter'), - ...baseChatModelSchema.shape, - }), - z.object({ - providerType: z.literal('ollama'), + providerType: z.literal('xai'), ...baseChatModelSchema.shape, }), z.object({ - providerType: z.literal('lm-studio'), + providerType: z.literal('deepseek'), ...baseChatModelSchema.shape, }), z.object({ - providerType: z.literal('deepseek'), + providerType: z.literal('mistral'), ...baseChatModelSchema.shape, }), z.object({ @@ -93,15 +85,15 @@ export const chatModelSchema = z.discriminatedUnion('providerType', [ .optional(), }), z.object({ - providerType: z.literal('mistral'), + providerType: z.literal('openrouter'), ...baseChatModelSchema.shape, }), z.object({ - providerType: z.literal('morph'), + providerType: z.literal('ollama'), ...baseChatModelSchema.shape, }), z.object({ - providerType: z.literal('xai'), + providerType: z.literal('lm-studio'), ...baseChatModelSchema.shape, }), z.object({ diff --git a/src/types/embedding-model.types.ts b/src/types/embedding-model.types.ts index b98da59a..5f89b5c4 100644 --- a/src/types/embedding-model.types.ts +++ b/src/types/embedding-model.types.ts @@ -33,19 +33,7 @@ export const embeddingModelSchema = z.discriminatedUnion('providerType', [ ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('groq'), - ...baseEmbeddingModelSchema.shape, - }), - z.object({ - providerType: z.literal('openrouter'), - ...baseEmbeddingModelSchema.shape, - }), - z.object({ - providerType: z.literal('ollama'), - ...baseEmbeddingModelSchema.shape, - }), - z.object({ - providerType: z.literal('lm-studio'), + providerType: z.literal('xai'), ...baseEmbeddingModelSchema.shape, }), z.object({ @@ -61,11 +49,15 @@ export const embeddingModelSchema = z.discriminatedUnion('providerType', [ ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('morph'), + providerType: z.literal('openrouter'), ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('xai'), + providerType: z.literal('ollama'), + ...baseEmbeddingModelSchema.shape, + }), + z.object({ + providerType: z.literal('lm-studio'), ...baseEmbeddingModelSchema.shape, }), z.object({ diff --git a/src/types/provider.types.ts b/src/types/provider.types.ts index ece01650..effaf1c1 100644 --- a/src/types/provider.types.ts +++ b/src/types/provider.types.ts @@ -28,15 +28,15 @@ export const llmProviderSchema = z.discriminatedUnion('type', [ ...baseLlmProviderSchema.shape, }), z.object({ - type: z.literal('deepseek'), + type: z.literal('xai'), ...baseLlmProviderSchema.shape, }), z.object({ - type: z.literal('perplexity'), + type: z.literal('deepseek'), ...baseLlmProviderSchema.shape, }), z.object({ - type: z.literal('groq'), + type: z.literal('perplexity'), ...baseLlmProviderSchema.shape, }), z.object({ @@ -55,14 +55,6 @@ export const llmProviderSchema = z.discriminatedUnion('type', [ type: z.literal('lm-studio'), ...baseLlmProviderSchema.shape, }), - z.object({ - type: z.literal('morph'), - ...baseLlmProviderSchema.shape, - }), - z.object({ - type: z.literal('xai'), - ...baseLlmProviderSchema.shape, - }), z.object({ type: z.literal('azure-openai'), ...baseLlmProviderSchema.shape, From 327a17eca3321614c75191d56a2f8c759dd71f8b Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:57:03 +0900 Subject: [PATCH 14/15] fix: fix lint errors --- src/settings/schema/migrations/1_to_2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings/schema/migrations/1_to_2.ts b/src/settings/schema/migrations/1_to_2.ts index 531bcf0d..3d91c4e7 100644 --- a/src/settings/schema/migrations/1_to_2.ts +++ b/src/settings/schema/migrations/1_to_2.ts @@ -663,7 +663,7 @@ export const migrateFrom1To2: SettingMigration['migrate'] = ( (v) => v.type === 'ollama' && v.baseUrl === data.ollamaApplyModel.baseUrl, ) - let ollamaApplyProviderId + let ollamaApplyProviderId: string if ( !existingSameOllamaProviderForApplyModel && data.ollamaApplyModel.baseUrl @@ -739,7 +739,7 @@ export const migrateFrom1To2: SettingMigration['migrate'] = ( v.apiKey === data.openAICompatibleApplyModel.apiKey, ) - let customProviderId + let customProviderId: string if (existingSameProvider) { // if the same provider is already exists, don't create a new one customProviderId = existingSameProvider.id From 3cc9b86028a989590288b20127f20a8eca066601 Mon Sep 17 00:00:00 2001 From: Kevin On <40454531+kevin-on@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:02:47 +0900 Subject: [PATCH 15/15] fix: Handle null content in GeminiProvider responses --- src/core/llm/gemini.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/llm/gemini.ts b/src/core/llm/gemini.ts index 170507ce..570708e3 100644 --- a/src/core/llm/gemini.ts +++ b/src/core/llm/gemini.ts @@ -347,7 +347,7 @@ export class GeminiProvider extends BaseLLMProvider< { finish_reason: chunk.candidates?.[0]?.finishReason ?? null, delta: { - content: chunk.text, + content: chunk.text ?? null, reasoning: reasoning, tool_calls: GeminiProvider.parseFunctionCallsForStreaming( chunk.functionCalls,